Удобная генерация URL (ЧПУ). Laravel 4 + сторонние пакеты

от автора

Хотелось бы поделиться удобными инструментами для генерации URL и примерами их использования.

Задача стоит не такая уж и большая, но она возникает постоянно, и хочется сократить время, затрачиваемое на написание велосипеда ее решение. Так же хочется избавиться от повсеместного использования вызовов разных классов, методов, функций и так далее при каждой необходимости сгенерировать 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *