Задача стоит не такая уж и большая, но она возникает постоянно, и хочется сократить время, затрачиваемое на написание велосипеда ее решение. Так же хочется избавиться от повсеместного использования вызовов разных классов, методов, функций и так далее при каждой необходимости сгенерировать URL. Ах да, я использую Laravel и инстументы заточены под него.
Ссылки на инструменты
Этого нам вполне хватит.
Постановка задачи
Автоматическая генерация уникальных URL для записей в таблицу БД для доступа к ним по /resource/unique-resource-url вместо /resource/1.
Приступаем
Допустим, нам нужно разбить поиск на сайте по Странам и Городам, но так, чтобы пользователь легко ориентировался, какая область/город выбран при просмотре списка Продуктов сайта.
Начнем с того, что создадим новый проект:
composer create-project laravel/laravel habr_url --prefer-dist
Далее откываем composer.json в корне habr_url и вносим пакеты в 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" добавим для быстрого прототипирования.
После выполняем комманду composer update в консоли, а после успешно установленных пакетов вносим изменения в 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', ), ); ?>
Класс Slug даст нам возможность генерировать URL из киррилицы, так как стандартный класс Str умеет работать только с латиницей. О 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. Открываем папку app/database/seeds и изменяем два файла:
// файл 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), который принимает $input как строку и генерирует из нее что-то на подобии moskva или 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'); } }
Перепишем методы store в CitiesController и 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.'); } // ... }
И уберем из app/views/cities/create.blade.php, app/views/cities/edit.blade.php, app/views/products/create.blade.php, app/views/products/edit.blade.php соответствующие елементы формы.
Отлично, URL генерируются, но что будет в случает с их дублированием? Возникнет ошибка. А чтобы этого избежать — при совпадении slug нам прийдется добавить префикс, а если префикс ужде есть — то инкрементировать его. Работы много, а элегантности нет. Чтобы избежать этих телодвижений воспользуемся пакетом Eloquent Sluggable.
Первым делом скинем себе в проект конфигурацию для Eloquent Sluggable:
php artisan config:publish cviebrock/eloquent-sluggable
В конфигурационном файле, который находится тут app/config/cviebrock/eloquent-sluggable/config.php изменим опцию 'method' => null на 'method' => array('Slug', 'make'). Таким образом, задача перевода из киррилических символов в транслит и создания URL возложится на класс Slug (вместо стандартного Str, который не умеет работать с киррилицей) и его метод make.
Чем хорош этот пакет? Он работает по такому принцыпу: ожидает, события eloquent.saving*, который отвечает за сохранение записи в БД, и записывает в поле, которое указано в настройках Модели сгенерированный slug. Пример конфигурации:
// файл 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, в новый будет добавлен префикс -1, -2, и так далее. К тому же, мы можем избавиться от не нужного правила для slug и в методе CitiesController@store убрать строчку $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, если перепишем $sluggable в Модели 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 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 добъемся желаемого результата:
// файл 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>{{{ $product->city->name }}}</td> </tr> </tbody> </table> @stop
На этом все.
Демо и Git
Ошибки, как обычно в личку. Предложения и критику — в комментарии. Спасибо за внимание.
ссылка на оригинал статьи http://habrahabr.ru/post/208678/
Добавить комментарий