📜 ⬆️ ⬇️

Laravel. Install, configure, create and deploy applications

So, you have the desire to try or learn about the Laravel framework.

If you are familiar with other PHP frameworks, it will not be difficult for you, but if not, this is an excellent choice for the first framework.

Laravel - PHP framework for artisans!
')
The article is very large. I recommend reading it completely during the weekend.

For the lazy:
Github
application



Installation


To install Laravel, we need Composer
Composer is a tool for managing dependencies in PHP . It allows you to declare dependent libraries needed for a project and install them into a project.
- Composer

The installation of the environment will take place in the *nix environment (the site also has a manual for installing on Windows , plus you will need a server, for example, WAMP and Git ).

Suppose that you have a very clean OS. Then open the terminal and enter these lines, copy and paste

 #    sudo apt-get update sudo apt-get install -y build-essential sudo apt-get install -y python-software-properties #    php 5.5 sudo add-apt-repository ppa:ondrej/php5 sudo apt-get update #   sudo apt-get install -y php5 sudo apt-get install -y apache2 sudo apt-get install -y libapache2-mod-php5 sudo apt-get install -y mysql-server sudo apt-get install -y php5-mysql sudo apt-get install -y php5-curl sudo apt-get install -y php5-gd sudo apt-get install -y php5-mcrypt sudo apt-get install -y git-core sudo apt-get install -y phpmyadmin #   phpmyadmin echo "Include /etc/phpmyadmin/apache.conf" | sudo tee -a /etc/apache2/apache2.conf #  mod_rewrite sudo a2enmod rewrite #  apache    sudo /etc/init.d/apache2 restart #   Composer curl -sS https://getcomposer.org/installer | php sudo mv composer.phar /usr/local/bin/composer 

After a while you will have all the necessary tools installed.
We proceed directly to the installation of Laravel .

 #     cd #    /home/%user% mkdir workspace #  workspace cd workspace #    mkdir php #   php cd php #    php 

Create a laravel project in the habr folder

 composer create-project laravel/laravel habr --prefer-dist # ....       .... 

Let's go over to the created project and make sure that everything works by running the php artisan serve command

 cd habr php artisan serve 

The local server will be available at http: // localhost: 8000 .

Just in case, artisan is a command line script found in Laravel . It provides a number of useful commands for use in development. It runs on top of the Symfony console component. ( Artisan CLI ). There are many useful commands that can be used to create various useful things on the command line. For a list of commands, enter php artisan list in the command line.

Going to http: // localhost: 8000 you should see a nice screensaver like at the beginning of the post.

Customization


To connect to the database (hereinafter the database), Laravel has the configuration file database.php , it is located in the app / config / folder.
First, create a database and user in MySQL

 mysql -u root -p #    > CREATE DATABASE `habr` CHARACTER SET utf8 COLLATE utf8_general_ci; > CREATE USER 'habr'@'localhost' IDENTIFIED BY 'my_password'; > GRANT ALL PRIVILEGES ON habr.* TO 'habr'@'localhost'; > exit 

Fine! We have all the data to access MySQL : the habr user with the password my_password and the habr database on localhost . Go to the database configuration file and change our settings.

Laravel database configuration file

Laravel has great tools - Migrations and Schema Builder .
Migrations are a type of version control in a database. They allow the development team to change the database schema and stay informed about the current state of the schema. Migration, as a rule, paired with the Schema Builder will allow you to easily manage the database schema.
- Migrations
The Schema Builder is a Schema class. It allows the manipulation of tables in the database. It works well with all databases that are supported by Laravel , and has a single API for all of these systems.
- Schema Builder

First, create a migration table:

 php artisan migrate:install 

If the database connection settings are correct, then we are ready to create migrations and tables.
But before that, I want to introduce you to installing additional packages that can be used to create a web application more efficiently and quickly.

Laravel 4 Generators

Mega useful tool - generators from Jeffrey Way . Github

He adds many useful commands to the artisan list, such as:



Package installation

Installing packages using Composer is quite simple. You need to edit the composer.json file in the application root by adding the line "way/generators": "1.*" to the "require" list.

 "require": { "laravel/framework": "4.1.*", "way/generators": "1.*" }, 

After that, you need to update the project dependencies. Enter in the terminal

 composer update 

The final touch will be putting in the configuration file app / config / app.php in the list of application providers line

 'Way\Generators\GeneratorsServiceProvider' 

Now the list of php artisan commands will also contain new generate commands. In the next section, I will show how to use generate to create an application and speed up development.

Create application


Suppose we create a blog site with discounts. For this we need:



Sketch the table schema in the database. I got something like this:
Initial DB Schema

Thanks for this generator 'y. Since all I did was register 10 lines, by the way, here they are:

 php artisan generate:migration create_users_table --fields="email:string:unique, password:string[60], username:string:unique, remember_token:string:nullable" php artisan generate:scaffold role --fields="role:string:unique" php artisan generate:pivot users roles php artisan generate:scaffold city --fields="name:string:unique" php artisan generate:scaffold company --fields="title:string:unique" php artisan generate:scaffold tag --fields="title:string:unique" php artisan generate:scaffold offer --fields="title:string, description:text, city_id:integer:unsigned, company_id:integer:unsigned, off:integer:unsigned, image:string, expires:date" php artisan generate:scaffold comment --fields="body:text, user_id:integer:unsigned, offer_id:integer:unsigned, mark:integer" php artisan generate:pivot offers tags #      php artisan migrate 

With the help of the last command, all migrations that have not yet been recorded will be entered in the database. The important thing is that all new migrations will be launched by one stack. In order to roll back the migration there is the php artisan migrate:rollback command, and in order to roll back all migrations to zero migrate:reset , to roll to zero and run all migrate migrate:refresh migrations.

In Laravel version higher than 4.1.25 , a security update occurred where a hole with stolen cookies was closed. Details of the update and instructions can be found here: http://laravel.com/docs/upgrade for those with a version of Laravel < 4.1.26 . Or just read the comments from vlom88 http://habrahabr.ru/post/197454/#comment_7510479 .


Read more about generator commands:



I hope this example of using the generator quite clearly showed how to use it and how useful it is.

What we still lack is some of the connections between the tables.
It is important to know! When adding a foreign key to a column in a table, you need to make sure that the column is unsigned.

Well, add them:

 php artisan generate:migration add_foreign_user_id_and_offer_id_to_comments_table php artisan generate:migration add_foreign_city_id_and_company_id_to_offers_table 

Now we need to register the addition of indexes inside the migration files themselves, since such changes are not automatically created.

 ... class AddForeignUserIdAndOfferIdToCommentsTable extends Migration { ... public function up() { Schema::table('comments', function(Blueprint $table) { $table->index('user_id'); $table->index('offer_id'); $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); $table->foreign('offer_id')->references('id')->on('offers')->onDelete('cascade'); }); } ... public function down() { Schema::table('comments', function(Blueprint $table) { $table->dropForeign('comments_user_id_foreign'); $table->dropForeign('comments_offer_id_foreign'); $table->dropIndex('comments_user_id_index'); $table->dropIndex('comments_offer_id_index'); }); } } ... class AddForeignCityIdAndCompanyIdToOffersTable extends Migration { ... public function up() { Schema::table('offers', function(Blueprint $table) { $table->index('city_id'); $table->index('company_id'); $table->foreign('city_id')->references('id')->on('cities')->onDelete('cascade'); $table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade'); }); } ... public function down() { Schema::table('offers', function(Blueprint $table) { $table->dropForeign('offers_city_id_foreign'); $table->dropForeign('offers_company_id_foreign'); $table->dropIndex('offers_city_id_index'); $table->dropIndex('offers_company_id_index'); }); } } 

Having taken a look at the DB scheme, we see the situation better.
Cool DB Schema

At the moment, all links to resources are open, and you can go to anyone for them.
Suppose we add the admin role. Following the link http: // localhost: 8000 / roles we see the following picture:
Admin role added

A bit about templates and Blade's template in Laravel.
For template files, the extension .blade.php is used . Looking into the file app / views / layouts / scaffold.blade.php we see

 // app/views/layouts/scaffold.blade.php <!doctype html> <html> <head> <meta charset="utf-8"> <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css" rel="stylesheet"> <style> table form { margin-bottom: 0; } form ul { margin-left: 0; list-style: none; } .error { color: red; font-style: italic; } body { padding-top: 20px; } </style> </head> <body> <div class="container"> @if (Session::has('message')) <div class="flash alert"> <p>{{ Session::get('message') }}</p> </div> @endif @yield('main') </div> </body> </html> 

What's going on here? The file itself is a skeleton, a layout, which can be expanded by adding some content inside the main section, or another template. Double curly braces {{$ var}} are analogous to <? Php echo $ var; ?> . The Session class is used here to display messages to the user if we pass a message. The message is temporary and will disappear when the page is refreshed. If we open the newly created template app / views / roles / index.blade.php

 // app/views/roles/index.blade.php @extends('layouts.scaffold') @section('main') <h1>All Roles</h1> <p>{{ link_to_route('roles.create', 'Add new role') }}</p> @if ($roles->count()) <table class="table table-striped table-bordered"> <thead> <tr> <th>Role</th> </tr> </thead> <tbody> @foreach ($roles as $role) <tr> <td>{{{ $role->role }}}</td> <td>{{ link_to_route('roles.edit', 'Edit', array($role->id), array('class' => 'btn btn-info')) }}</td> <td> {{ Form::open(array('method' => 'DELETE', 'route' => array('roles.destroy', $role->id))) }} {{ Form::submit('Delete', array('class' => 'btn btn-danger')) }} {{ Form::close() }} </td> </tr> @endforeach </tbody> </table> @else There are no roles @endif @stop 

It will become clear to us that this template extends the template app / views / layouts / scaffold.blade.php , the code @extends('layouts.scaffold') speaks for it. Note that here a point is used to separate folders, although you can also use / .

Further, in the main section, everything will be recorded until the first appearance of @stop . Also here we use the familiar if - else - endif and foreach - endforeach , the auxiliary function link_to_route , which Laravel provides for us (Helper Functions) and the Form class to create forms (preferably you need to use it, at least Form :: open (), so how it creates an additional attribute of the _token form - protection against forgery of cross- site requests and _method in the case of PUT / PATCH or DELETE).

First of all we will think about the protection of all resources. For this we need to enter the authorization.

Create a new LoginContoller controller in the app / controllers folder

 php artisan generate:controller LoginController 

And add some templates for it.

 mkdir app/views/login php artisan generate:view index --path="app/views/login" php artisan generate:view register --path="app/views/login" php artisan generate:view dashboard --path="app/views/login" 

Now we change the controller itself. We need 5 methods:

The modified LoginController controller will look like this:

 // app/controllers/LoginController.php class LoginController extends BaseController { /** * Login Form. * * @return Response */ public function index() { return View::make('login.index'); } /** * Registration form. * * @return Response */ public function register() { return View::make('login.register'); } /** * Registring new user and storing him to DB. * * @return Response */ public function store() { $rules = array( 'email' => 'required|email|unique:users,email', 'password' => 'required|alpha_num|between:4,50', 'username' => 'required|alpha_num|between:2,20|unique:users,username' ); $validator = Validator::make(Input::all(), $rules); if($validator->fails()){ return Redirect::back()->withInput()->withErrors($validator); } $user = new User; $user->email = Input::get('email'); $user->username = Input::get('username'); $user->password = Hash::make(Input::get('password')); $user->save(); Auth::loginUsingId($user->id); return Redirect::home()->with('message', 'Thank you for registration, now you can comment on offers!'); } /** * Log in to site. * * @return Response */ public function login() { if (Auth::attempt(array('email' => Input::get('email'), 'password' => Input::get('password')), true) || Auth::attempt(array('username' => Input::get('email'), 'password' => Input::get('password')), true)) { return Redirect::intended('dashboard'); } return Redirect::back()->withInput(Input::except('password'))->with('message', 'Wrong creadentials!'); } /** * Log out from site. * * @return Response */ public function logout() { Auth::logout(); return Redirect::home()->with('message', 'See you again!'); } } 

The first two methods are generated from HTML templates.
The store method saves a new user to our database, accepting all incoming data via POST from Input::all() . ( More info ).
The Input class contains the data that was sent during the POST request. It has a number of static methods, such as all() , get() , has() and others ( Basic Input ).

Hash is an encryption class that uses the bcrypt method to store passwords in the database in an encrypted form ( Laravel Security ).

But before registering, we need to validate the incoming data.
For this, Laravel has a class Validator . The Validation::make method takes 2 or 3 arguments:
  1. $input - required, array of input data to be checked
  2. $rules - required, array with rules for incoming data
  3. $messages - optional, error message array

A complete list of available rules can be found here Available Validation Rules .

The fails() method returns true or false depending on whether the data passed validation in accordance with the rules that we passed to the make method.

The Redirect class is used for redirection. His methods are:


The Auth class is an authorization class, it has a number of methods, including loginUsingId($id) , which authorizes the user by the specified identifier from the database ( Authenticating Users ). Since after registration we want to automatically authorize the user, we will use it.

The login() method of our Controller authorizes the user by email or username and redirects to the page from which he came under the authorization filter. If the data does not match, it redirects back with incoming data, an error message, but without a password.

So we have a controller that is responsible for authorization.

The next step to hide all resources from access will be to modify the app / routes.php file , which contains the application routes.

 // app/routes.php ... Route::get('/', array('as' => 'home', function() { return View::make('hello'); })); Route::get('logout', array('as' => 'login.logout', 'uses' => 'LoginController@logout')); Route::group(array('before' => 'un_auth'), function() { Route::get('login', array('as' => 'login.index', 'uses' => 'LoginController@index')); Route::get('register', array('as' => 'login.register', 'uses' => 'LoginController@register')); Route::post('login', array('uses' => 'LoginController@login')); Route::post('register', array('uses' => 'LoginController@store')); }); Route::group(array('before' => 'admin.auth'), function() { Route::get('dashboard', function() { return View::make('login.dashboard'); }); Route::resource('roles', 'RolesController'); Route::resource('cities', 'CitiesController'); Route::resource('companies', 'CompaniesController'); Route::resource('tags', 'TagsController'); Route::resource('offers', 'OffersController'); Route::resource('comments', 'CommentsController'); }); Route::filter('admin.auth', function() { if (Auth::guest()) { return Redirect::to('login'); } }); Route::filter('un_auth', function() { if (!Auth::guest()) { Auth::logout(); } }); 

Clicking now on the link, for example / roles, we will be redirected to the / login page, which "index.blade.php" displays only the standard text "index.blade.php" .

For all routes enclosed in Route::group(array('before' => 'admin.auth')) , the admin.auth filter will be applied, which checks whether the user is a guest or not and if it is - send it to the login page. You can read about filters here , but about grouping routes here . The other filter Route::group(array('before' => 'un_auth')) will check if the user is logged on to the site, and if the check is performed, then he will log him out.

For normal operation, change the login and registration files:

 // app/views/login/index.blade.php @extends('layouts.scaffold') @section('main') <h1>Login</h1> <p>{{ link_to_route('login.register', 'Register') }}</p> {{ Form::open(array('route' => 'login.index')) }} <ul> <li> {{ Form::label('email', 'Email or Username:') }} {{ Form::text('email') }} </li> <li> {{ Form::label('password', 'Password:') }} {{ Form::password('password') }} </li> <li> {{ Form::submit('Submit', array('class' => 'btn btn-info')) }} </li> </ul> {{ Form::close() }} @include('partials.errors', $errors) @stop // app/views/login/register.blade.php @extends('layouts.scaffold') @section('main') <h1>Register</h1> <p>{{ link_to_route('login.index', 'Login') }}</p> {{ Form::open(array('route' => 'login.register')) }} <ul> <li> {{ Form::label('email', 'Email:') }} {{ Form::text('email') }} </li> <li> {{ Form::label('username', 'Username:') }} {{ Form::text('username') }} </li> <li> {{ Form::label('password', 'Password:') }} {{ Form::password('password') }} </li> <li> {{ Form::submit('Submit', array('class' => 'btn btn-info')) }} </li> </ul> {{ Form::close() }} @include('partials.errors', $errors) @stop // app/views/login/dashboard.blade.php @extends('layouts.scaffold') @section('main') <h1>Administrative Dashboard</h1> <p>Nice to see you, <b>{{{ Auth::user()->username }}}</b></p> @stop // app/views/partials/errors.blade.php @if ($errors->any()) <ul> {{ implode('', $errors->all('<li class="error">:message</li>')) }} </ul> @endif 

As you noticed, here I used a new trick in the @include('view', $variable) template @include('view', $variable) . In the application it is very simple - pass 2 arguments:
  1. view - the template to be included in a specific template
  2. $ variable - the variable to be passed to draw the template

Register on the site to have access to the site.

Well, now you can do resources. Let's start with the cities. First of all, in the City Model we will change the validation rules:

 // app/models/City.php class City extends Eloquent { protected $guarded = array(); public static $rules = array( 'name' => 'required|alpha|min:2|max:200|unique:cities,name' ); } 

After it, we will change the validation rules for the Models Companyas Rolewell , and Tag:

 // app/models/Company.php ... public static $rules = array( 'name' => 'required|alpha|min:2|max:200|unique:companies,name' ); ... // app/models/Role.php ... public static $rules = array( 'role' => 'required|alpha|min:2|max:200|unique:roles,role' ); ... // app/models/Tag.php ... public static $rules = array( 'name' => 'required|min:2|max:200|unique:tags,name' ); ... 

For the convenience of navigating between links, add a menu to app / views / layouts / scaffold.blade.php , as well as add jQuery and jQuery-UI for future needs

 // app/views/layouts/scaffold.blade.php <!doctype html> <html> <head> <meta charset="utf-8"> <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css" rel="stylesheet"> <link href="//code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css" rel="stylesheet"> <style> table form { margin-bottom: 0; } form ul { margin-left: 0; list-style: none; } .error { color: red; font-style: italic; } body { padding-top: 20px; } input, textarea, .uneditable-input {width: 50%; min-width: 200px;} </style> @yield('styles') </head> <body> <div class="container"> <ul class="nav nav-pills"> <li>{{ link_to_route('offers.index', 'Offers') }}</li> <li>{{ link_to_route('tags.index', 'Tags') }}</li> <li>{{ link_to_route('roles.index', 'Roles') }}</li> <li>{{ link_to_route('cities.index', 'Cities') }}</li> <li>{{ link_to_route('comments.index', 'Comments') }}</li> <li>{{ link_to_route('companies.index', 'Companies') }}</li> <li class="pull-right">{{ link_to_route('login.logout', 'Logout') }}</li> </ul> @if (Session::has('message')) <div class="flash alert"> <p>{{ Session::get('message') }}</p> </div> @endif @yield('main') </div> <script type="text/javascript" src="//code.jquery.com/jquery.min.js"></script> <script type="text/javascript" src="//code.jquery.com/ui/1.10.3/jquery-ui.min.js"></script> @yield('scripts') </body> </html> 

Next, we proceed to editing the validation rules in the Model Offer:

 // app/models/Offer.php ... public static $rules = array( 'title' => 'required|between:5,200', 'description' => 'required|min:10', 'city_id' => 'required|exists:cities,id', 'company_id' => 'required|exists:companies,id', 'off' => 'required|numeric|min:1|max:100', 'image' => 'required|regex:/\/images\/\d{4}\/\d{2}\/\d{2}\/([A-z0-9]){30}\.jpg/', // matches /images/2012/12/21/ThisIsTheEndOfTheWorldMaya2112.jpg 'expires' => 'required|date' ); 

Here I used a complex pattern for the field image, because I want to use the tools AJAXfor uploading pictures, and pass only the path to the picture on the server to the validation itself. So let's start by changing the app / views / offers / create.blade.php template and creating a separate file for the scripts.

 // app/views/offers/create.blade.php ... {{ Form::label('file', 'Image:') }} {{ Form::file('file')}} <img src="" id="thumb" style="max-width:300px; max-height: 200px; display: block;"> {{ Form::hidden('image') }} <div class="error"></div> ... @section('scripts') @include('offers.scripts') @stop // app/views/offers/scripts.blade.php <script> $(document).ready(function(){ //     $('#expires').datepicker({dateFormat: "yy-mm-dd"}); var uploadInput = $('#file'), //    imageInput = $('[name="image"]'), //   URL  thumb = document.getElementById('thumb'), //   error = $('div.error'); //      uploadInput.on('change', function(){ //     FormData var data = new FormData(); //      data.append('file', uploadInput[0].files[0]); //    $.ajax({ //   URL    url: '/upload', //   type: 'POST', //     data: data, //     jQuery   processData: false, //     jQuery    contentType: false, //      dataType: 'json', //      success: function(result) { //     (    result) //      filelink if (result.filelink) { //   URL    thumb.setAttribute('src', result.filelink); //    input' imageInput.val(result.filelink); //   error.hide(); } else { //      error.text(result.message); error.show(); } }, // -    error: function (result) { //     error.text("Upload impossible"); error.show(); } }); }); }); </script> 

Here we will add the image by pressing the input[name="file"]and send it using the AJAXon URL/ upload. The response from this URL will be a link to the uploaded image. We will insert this link in the src attribute of the #thumb image and save it in a hidden input image. Next we need to add a route in the file app / routes.php upload:

 // app/routes.php ... Route::group(array('before' => 'admin.auth'), function(){ ... Route::resource('comments', 'CommentsController'); Route::post('upload', array('uses' => 'HomeController@uploadOfferImage')); } ... 

Ok, we have registered the URL, it remains to register the logic in HomeController. To do this, in the file app / controllers / HomeController.php add the uploadOfferImage
min method :
 // app/controllers/HomeController.php class HomeController extends BaseController { ... public function uploadOfferImage() { $rules = array('file' => 'mimes:jpeg,png'); $validator = Validator::make(Input::all(), $rules); if ($validator->fails()) { return Response::json(array('message' => $validator->messages()->first('file'))); } $dir = '/images'.date('/Y/m/d/'); do { $filename = str_random(30).'.jpg'; } while (File::exists(public_path().$dir.$filename)); Input::file('file')->move(public_path().$dir, $filename); return Response::json(array('filelink' => $dir.$filename)); } } 

Everything is quite simple: the rules, validation, errors, the answer. To save for a start, we will set the folder in which we will save it - this is public_path () / images / current year / month / date / (( public_path()this is an auxiliary function Laravelfor the path to public files), then we will create a random file name with a str_random(30)length of 30 characters and extension jpg. After that, we use the class Inputand its method file('file')->move('destination_path', 'filename'), where: 'file' is the incoming file, 'destination_path' is the folder to which we are moving the file, 'filename' is the name for the file to be saved.
Response::jsonwill give a response in the format json.
Fine!Files are now loaded with AJAX.
AJAX upload Laravel
The next step will be a change Form::input('number', 'city_id'), and Form::input('number', 'company_id')at Selecta with real data.

 // app/views/offers/create.blade.php ... <?php $cities = array(0 => 'Choose city'); foreach (City::get(array('id', 'name')) as $city) { $cities[$city->id] = $city->name; } ?> <li> {{ Form::label('city_id', 'City_id:') }} {{ Form::select('city_id', $cities) }} </li> <?php $companies = array(0 => 'Choose company'); foreach (Company::get(array('id', 'name')) as $company) { $companies[$company->id] = $company->name; } ?> <li> {{ Form::label('company_id', 'Company_id:') }} {{ Form::select('company_id', $companies) }} </li> ... 

How selects work you can see here Forms & Html (Dropdown Lists) . Thus, we have the opportunity to choose from existing cities and companies in the database.

What we still lack is the addition of tags to discounts. This is where jquery-ui will help us with autocomplete to add multiple values. To do this, expand the script file app / views / offers / create.blade.php :

 // app/views/offers/scripts.blade.php <script> $(document).ready(function(){ ... function split( val ) { return val.split( /,\s*/ ); } function extractLast( term ) { return split( term ).pop(); } $( "#tags" ) // don't navigate away from the field on tab when selecting an item .bind( "keydown", function( event ) { if ( event.keyCode === $.ui.keyCode.TAB && $( this ).data( "ui-autocomplete" ).menu.active ) { event.preventDefault(); } }) .autocomplete({ source: function( request, response ) { $.getJSON( "/tags", { term: extractLast( request.term ), }, function( data ) { response($.map(data, function(item) { return { value: item.name } })) } ); }, search: function() { // custom minLength var term = extractLast( this.value ); if ( term.length < 2 ) { return false; } }, focus: function() { // prevent value inserted on focus return false; }, select: function( event, ui ) { console.log(ui); console.log(this); var terms = split( this.value ); // remove the current input terms.pop(); // add the selected item terms.push( ui.item.value ); // add placeholder to get the comma-and-space at the end terms.push( "" ); this.value = terms.join( ", " ); return false; } }); }); </script> 

This is a standard use case from jqueryui.com , only slightly modified at the response point from the server. As you can see, the appeal goes to the address /tags. We organize the logic of the response to the AJAXrequest for this URL.

 // app/controllers/TagController.php class TagsController extends BaseController { ... /** * Display a listing of the resource. * * @return Response */ public function index() { $tags = $this->tag->all(); //   AJAX  if (Request::ajax()) { //    ,      $tags = Tag::where('name', 'like', '%'.Input::get('term', '').'%')->get(array('name')); //     json return $tags; } return View::make('tags.index', compact('tags')); } ... 

Interestingly, what is Eloquentconverted to a format jsonif we return it, so there is no need to use it Response::json(). And here we auto-complete tags.

The last thing we need to do is change the logic for creating discounts.
 // app/controllers/OffersController.php class OffersController extends BaseController { ... /** * Store a newly created resource in storage. * * @return Response */ public function store() { $rules = Offer::$rules; $rules['expires'] .= '|after:'.date('Ym-d', strtotime('+1 day')).'|before:'.date('Ym-d', strtotime('+1 month')); $validation = Validator::make(Input::all(), $rules); if ($validation->passes()) { $tags = array(); foreach (explode(', ', Input::get('tags')) as $tag_name) { if ($tag = Tag::where('name', '=', $tag_name)->first()) { $tags[] = $tag->id; } } if (count($tags) == 0) { return Redirect::route('offers.create') ->withInput() ->with('message', 'Insert at least one tag.'); } $offer = $this->offer->create(Input::except('tags', 'file')); $offer->tags()->sync($tags); return Redirect::route('offers.index'); } return Redirect::route('offers.create') ->withInput() ->withErrors($validation) ->with('message', 'There were validation errors.'); } ... 

First, we expand the expires rule , so that the discount ends not earlier than tomorrow, and no later than in 1 month. Next, select all the idtags in a separate array, checking their presence in the database. After there is a small check whether tags are entered. And at the very end there is a very interesting trick: in Eloquent for a bunch of tables, you can use different relationships ( Eloquent Relationships ), for example, the Model Offers can have many tags, respectively, we write it in the Model

 // app/models/Offer.php ... public function tags() { return $this->belongsToMany('Tag'); } ... 

Thus, we have created a link between one entry in the offers table and many entries in the tags table . Now, turning to the method, $offer->tags()we can get all the tags to which a specific discount is attached. But in this example, we still use a special method for working with intermediate tables sync(array(1, 2, 3)), which will be written into the intermediate table as offer_idneeded tag_id. Table offer_tag :
Pivot table offer to tag
We also need to specify the link between the record in the offers table and the records in the cities and companies tables :

 // app/models/Offer.php ... public function city() { return $this->belongsTo('City'); } public function company() { return $this->belongsTo('Company'); } public function tags() { return $this->belongsToMany('Tag'); } //         +     public function webDescription($options = array()) { $str = $this->description; if (isset($options['shorten'])) { $length = isset($options['length']) ? (int) $options['length'] : 250; $end = isset($options['end']) ? : '…'; if (mb_strlen($str) > $length) { $str = mb_substr(trim($str), 0, $length); $str = mb_substr($str, 0, mb_strlen($str) - mb_strpos(strrev($str), ' ')); $str = trim($str.$end); } } $str = str_replace("\r\n", '<br>', e($str)); return $str; } } 

It remains to change the file app / views / offers / index.blade.php

 // app/views/offers/index.blade.php @if ($offers->count()) <table class="table table-striped table-bordered"> <thead> <tr> <th>Title</th> <th>Description</th> <th>City</th> <th>Company</th> <th>Off</th> <th>Image</th> <th>Tags</th> <th>Expires</th> </tr> </thead> <tbody> @foreach ($offers as $offer) <tr> <td>{{{ $offer->title }}}</td> <td>{{ $offer->webDescription(array('shorten' => true, 'length' => 60)) }}</td> <td>{{{ $offer->city->name }}}</td> <td>{{{ $offer->company->name }}}</td> <td>{{{ $offer->off }}}</td> <td><img src="" style="max-width: 200px; max-height:150px;"></td> <td> @foreach($offer->tags as $tag) <span class="badge">{{{$tag->name}}}</span> @endforeach </td> <td>{{{ $offer->expires }}}</td> <td> {{ link_to_route('offers.edit', 'Edit', array($offer->id), array('class' => 'btn btn-info')) }} </td> <td> {{ Form::open(array('method' => 'DELETE', 'route' => array('offers.destroy', $offer->id))) }} {{ Form::submit('Delete', array('class' => 'btn btn-danger')) }} {{ Form::close() }} </td> </tr> @endforeach </tbody> </table> @else There are no offers @endif 

And we see an excellent picture that fully reflects the structure of the discount:
All offers
{{{$ string}}} displays the contents of $ string , having previously run through htmlentities, I mean, converts non-safe characters, which protects them from XSS. Analog is <?php echo htmlentities($string); ?>or auxiliary functionLaravel e($string)


Now it remains to change app / views / offers / edit.blade.php , app / views / offers / show.blade.php and the method updatein app / controllers / OfferController.php .

Code for app / views / edit.blade.php
 // app/views/offers/edit.blade.php @extends('layouts.scaffold') @section('main') <h1>Edit Offer</h1> {{ Form::model($offer, array('method' => 'PATCH', 'route' => array('offers.update', $offer->id))) }} <ul> <li> {{ Form::label('title', 'Title:') }} {{ Form::text('title') }} </li> <li> {{ Form::label('description', 'Description:') }} {{ Form::textarea('description') }} </li> <?php $cities = array(0 => 'Choose city'); foreach (City::get(array('id', 'name')) as $city) { $cities[$city->id] = $city->name; } ?> <li> {{ Form::label('city_id', 'City_id:') }} {{ Form::select('city_id', $cities) }} </li> <?php $companies = array(0 => 'Choose company'); foreach (Company::get(array('id', 'name')) as $company) { $companies[$company->id] = $company->name; } ?> <li> {{ Form::label('company_id', 'Company_id:') }} {{ Form::select('company_id', $companies) }} </li> <li> {{ Form::label('off', 'Off:') }} {{ Form::input('number', 'off') }} </li> <li> {{ Form::label('file', 'Image:') }} {{ Form::file('file')}} <img src="" id="thumb" style="max-width:300px; max-height: 200px; display:block; "> {{ Form::hidden('image') }} <div class="error"></div> </li> <li> {{ Form::label('expires', 'Expires:') }} {{ Form::text('expires') }} </li> <li> {{ Form::label('tags', 'Tags:') }} {{ Form::text('tags', Input::old('tags', implode(', ', array_fetch($offer->tags()->get(array('name'))->toArray(), 'name')))) }} </li> <li> {{ Form::submit('Update', array('class' => 'btn btn-info')) }} {{ link_to_route('offers.show', 'Cancel', $offer->id, array('class' => 'btn')) }} </li> </ul> {{ Form::close() }} @if ($errors->any()) <ul> {{ implode('', $errors->all('<li class="error">:message</li>')) }} </ul> @endif @stop @section('scripts') @include('offers.scripts') @stop 

The changes are very similar to app / views / offers / create.blade.php , only there is a slight difference in and {{ Form::text('tags', ... }}. Everything is clear with the picture: if there is an old input - replace it with it, if it doesn’t, then with the value of imageour discount. In the Form::text('tags', ... )first place, we took all the tags that relate to a particular discount $offer->tags()and removed only the fields from the database name. Next, we used the auxiliary function from Laravel array_fetch , so that we would have a one-dimensional array, and at the end we connected this array to the string, inserting a comma and a space between them.

Change the method updateto OfferController:

 // app/controllers/OfferController.php class OffersController extends BaseController { ... public function update($id) { $offer = $this->offer->findOrFail($id); $rules = Offer::$rules; $rules['expires'] .= '|after:'.date('Ym-d', strtotime('+1 day')).'|before:'.date('Ym-d', strtotime('+1 month')); $validation = Validator::make(Input::all(), $rules); if ($validation->passes()) { $tags = array(); foreach (explode(', ', Input::get('tags')) as $tag_name) { if ($tag = Tag::where('name', '=', $tag_name)->first()) { $tags[] = $tag->id; } } if (count($tags) == 0) { return Redirect::route('offers.create') ->withInput() ->withErrors($validation) ->with('message', 'Insert at least one tag.'); } $offer->update(Input::except('tags', 'file', '_method')); $offer->tags()->sync($tags); return Redirect::route('offers.show', $id); } return Redirect::route('offers.edit', $id) ->withInput() ->withErrors($validation) ->with('message', 'There were validation errors.'); } ... 

The difference with the method of addition is minimal. First, we will throw away the 404 error, if the wrong one is given id, secondly we will use the method update($id). That's all the change.

Next, change the file app / views / offers / show.blade.php :

 // app/views/offers/show.blade.php ... <thead> <tr> <th>Title</th> <th>Description</th> <th>City_id</th> <th>Company_id</th> <th>Off</th> <th>Image</th> <th>Tags</th> <th>Expires</th> </tr> </thead> <tbody> <tr> <td>{{{ $offer->title }}}</td> <td>{{ $offer->webDescription(array('shorten' => true, 'length' => 60)) }}</td> <td>{{{ $offer->city->name }}}</td> <td>{{{ $offer->company->name }}}</td> <td>{{{ $offer->off }}}</td> <td><img src="" style="max-width: 200px; max-height:150px;"/></td> <td> @foreach($offer->tags as $tag) <span class="badge">{{{ $tag->name }}}</span> @endforeach </td> <td>{{{ $offer->expires }}}</td> ... 

Now and after changing the discount, we will have a nice display of its structure with an image and all relational data.

home page of the site

The time has finally come to create the main page of the site.

First, create a new one layout:

 // app/views/layouts/main.blade.php <!doctype html> <html> <head> <meta charset="utf-8"> <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css" rel="stylesheet"> <link rel="stylesheet" type="text/css" href="{{ asset('css/main.css') }}"> @yield('styles') </head> <body> <div class="navbar navbar-fixed-top"> <div class="navbar-inner"> <div class="container"> <a class="brand" href="{{ route('home') }}">Habr Offers</a> <ul class="nav"> <li><a href="{{ route('home') }}">Home</a></li> </ul> </div> </div> </div> <div class="container"> @if (Session::has('message')) <div class="flash alert"> <p>{{ Session::get('message') }}</p> </div> @endif @yield('main') </div> <script type="text/javascript" src="//code.jquery.com/jquery.min.js"></script> <script type="text/javascript" src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/js/bootstrap.min.js"></script> @yield('scripts') </body> </html> 

As well as the style file:

 // public/css/main.css /*        -     */ body {padding-top: 60px;} /*  ,      */ .no_decoration:hover, .no_decoration:focus {text-decoration: none;} /*           /  */ .thumbnail .image-container {width: 100%; max-height: 200px; overflow: hidden;} .thumbnail .image-container img {min-width: 100%; min-height: 100%;} .thumbnail h3 {height: 40px; overflow: hidden;} .thumbnail .description {height: 100px; overflow: hidden;} 

Then we redefine the main page route:

 // app/routes.php Route::get('/', array('as' => 'home', 'uses' => 'HomeController@index')); 

Add to the HomeControllermissing method index:

 // app/controllers/HomeController.php ... /** * Display a listing of offers. * * @return Response */ public function index() { $offers = Offer::orderBy('created_at', 'desc')->get(); return View::make('home.index', compact('offers')); } ... 

Create the app / views / home folder and add the index.blade.php file there , as well as create the _preview.blade.php file in the app / views / offers folder

 // app/views/home/index.blade.php @extends('layouts.main') @section('main') <h1>{{ $title }}</h1> @if ($offers->count()) @foreach ($offers as $key => $offer) @if($key % 3 == 0) <div class="row-fluid"> <ul class="thumbnails"> @endif <li class="span4"> <div class="thumbnail"> @include('offers._preview', $offer) </div> </li> @if($key % 3 == 2 || $key == count($offers) - 1) </ul> </div> @endif @endforeach @else There are no offers @endif @stop // app/views/offers/_preview.blade.php <div class="image-container"> <img src=""> </div> <div class="caption"> <h3>{{{ $offer->title }}}</h3> <hr> <p class="description">{{ $offer->webDescription() }}</p> <hr> <p><span class="label label-important">{{{ $offer->off }}} % off</span></p> <p>Location: {{{ $offer->city->name }}}</p> <p>Offer by: {{{ $offer->company->name }}}</p> <p>Expires on: <span class="label label-warning">{{{ $offer->expires }}}</span></p> <p>Tags: @foreach($offer->tags as $tag) <span class="badge">{{{$tag->name}}}</span> @endforeach </p> </div> 

Next you need to add a search for discounts on tags, cities and companies. To do this, add 3 routes to the file app / routes.php immediately after home:

 // app/routes.php ... Route::get('by_tag/{name}', array('as' => 'home.by_tag', 'uses' => 'HomeController@byTag'))->where('name', '[A-Za-z0-9 -_]+'); Route::get('by_city/{name}', array('as' => 'home.by_city', 'uses' => 'HomeController@byCity'))->where('name', '[A-Za-z0-9 -_]+'); Route::get('by_company/{name}', array('as' => 'home.by_company', 'uses' => 'HomeController@byCompany'))->where('name', '[A-Za-z0-9 -_]+'); ... 

Now add the missing methods to HomeController:

 // app/controllers/HomeController.php ... /** * Display a listing of offers that belongs to tag. * * @param string $name * @return Response */ public function byTag($name) { $tag = Tag::whereName($name)->firstOrFail(); $offers = $tag->offers; $title = "Offers tagged as: " . $tag->name; return View::make('home.index', compact('offers', 'title')); } /** * Display a listing of offers that belongs to city. * * @param string $name * @return Response */ public function byCity($name) { $city = City::whereName($name)->firstOrFail(); $offers = $city->offers; $title = "Offers in: " . $city->name; return View::make('home.index', compact('offers', 'title')); } /** * Display a listing of offers that belongs to company. * * @param string $name * @return Response */ public function byCompany($name) { $company = Company::whereName($name)->firstOrFail(); $offers = $company->offers; $title = "Offers by: " . $company->name; return View::make('home.index', compact('offers', 'title')); } ... 

For these methods to work correctly, we need to define the links in the Models City, Companyand Tag:

 // app/models/City.php ... public function offers() { return $this->hasMany('Offer'); } // app/models/Company.php ... public function offers() { return $this->hasMany('Offer'); } // app/models/Tag.php ... public function offers() { return $this->belongsToMany('Offer'); } 

To make this whole thing play, change the file app / views / offers / _preview.blade.php , adding the links:

 // app/views/offers/_preview.blade.php <a class="image-container" href="{{ route('home.offer', $offer->id) }}"> <img src=""> </a> <div class="caption"> <h3>{{{ $offer->title }}}</h3> <hr> <p class="description">{{ $offer->webDescription() }}</p> <hr> <p><span class="label label-important">{{{ $offer->off }}} % off</span></p> <p>Location: <a href="{{ route('home.by_city', $offer->city->name) }}">{{{ $offer->city->name }}}</a></p> <p>Offer by: <a href="{{ route('home.by_company', $offer->company->name) }}">{{{ $offer->company->name }}}</a></p> <p>Expires on: <span class="label label-warning">{{{ $offer->expires }}}</span></p> <p>Tags: @foreach($offer->tags as $tag) <a class="no_decoration" href="{{ route('home.by_tag', $tag->name) }}"> <span class="badge">{{{$tag->name}}}</span> </a> @endforeach </p> </div> 

Click, go, discounts are sorted and displayed in accordance with the criteria.

Now we will make a presentation for viewing a separate discount:

 // app/views/offers/_show.blade.php @extends('layouts.main') @section('main') <div class="page-header"> <h1> <span class="label label-important label-big">{{{ $offer->off }}}%</span> {{{ $offer->title }}} <small> by <a href="{{{ route('home.by_company', $offer->company->name) }}}">{{{ $offer->company->name }}}</a> </small> </h1> </div> <div class="pull-left image-container-big"> <img class="img-rounded" src="" alt="{{{ $offer->title }}}"> </div> <div class="description"> <p>{{ $offer->webDescription() }}</p> </div> <div class="clearfix"></div> <hr> <p>Location: <a href="{{ route('home.by_city', $offer->city->name) }}">{{{ $offer->city->name }}}</a> </p> <p>Tags: @foreach($offer->tags as $tag) <a class="no_decoration" href="{{ route('home.by_tag', $tag->name) }}"> <span class="badge">{{{$tag->name}}}</span> </a> @endforeach </p> <hr> <div class="page-header"> <h3>User's comments <small>leave and yours one</small></h3> </div> {{ Form::open() }} {{ Form::textarea('body', Input::old('body'), array('class' => 'input-block-level', 'style' => 'resize: vertical;'))}} <div class="input-append"> {{ Form::select('mark', array(0 => 5, 1 => 4, 2 => 3, 3 => 2, 4 => 1), Input::old('mark', 0)) }} {{ Form::submit('Comment', array('class' => 'btn btn-success', 'style' => 'clear: both;')) }} </div> {{ Form::close() }} @include('partials.errors', $errors) @stop // public/css/main.css    body {padding-top: 60px;} .error {color: red;} .no_decoration:hover, .no_decoration:focus {text-decoration: none;} .thumbnail .image-container {width: 100%; max-height: 200px; overflow: hidden; display: block;} .thumbnail .image-container img {min-width: 100%; min-height: 100%;} .thumbnail h3 {height: 40px; overflow: hidden;} .thumbnail .description {height: 100px; overflow: hidden;} .image-container-big {width: 500px; height: 300px; margin: 0 20px 20px 0; text-align: center;} .image-container-big img {max-height: 300px; margin: 0 auto;} .label.label-big {font-size: 32px; line-height: 1.5em; padding: 0 15px; margin-bottom: 5px;} 

In order to view the discount in full, we will add a route and a method, as well as at the end I added a form for comments. For its operation, you also need to add a route and method in the desired controller:

 // app/routes.php ... Route::get('offer_{id}', array('as' => 'home.offer', 'uses' => 'HomeController@showOffer'))->where('id', '[0-9]+'); Route::post('offer_{id}', array('before' => 'not_guest', 'uses' => 'HomeController@commentOnOffer'))->where('id', '[0-9]+'); ... Route::filter('not_guest', function(){ if (Auth::guest()) { return Redirect::back()->withInput()->with('message', 'You should be logged in to provide this action.'); } }); // app/controllers/HomeController.php ... /** * Display an offer. * * @param int $id * @return Response */ public function showOffer($id) { $offer = Offer::findOrFail($id); return View::make('offers._show', compact('offer')); } /** * Storing comment on offer. * * @param int $id * @return Response */ public function commentOnOffer($id) { $offer = Offer::findOrFail($id); if ($offer->usersComments->contains(Auth::user()->id)) { return Redirect::back()->withInput()->with('message', 'You have already commented on this Offer'); } $rules = array('body' => 'required|alpha|min:10|max:500', 'mark' => 'required|numeric|between:1,5'); $validator = Validator::make(Input::all(), $rules); if ($validator->passes()) { $offer->usersComments()->attach(Auth::user()->id, array('body' => Input::get('body'), 'mark' => Input::get('mark'))); return Redirect::back(); } return Redirect::back()->withInput()->withErrors($validator); } ... 

We will understand everything in order:

To display comments on the discount page, let's modify the app / views / offers / _show.blade.php file a bit.

 // app/views/offers/_show.blade.php ... @if(!$offer->usersComments->count()) <div class="well">You can be first to comment on this offer!</div> @endif @if(Auth::guest() || (!Auth::guest() && !$offer->usersComments->contains(Auth::user()->id))) {{ Form::open() }} {{ Form::textarea('body', Input::old('body'), array('class' => 'input-block-level', 'style' => 'resize: vertical;'))}} <div class="input-append"> {{ Form::select('mark', array(5 => 5, 4 => 4, 3 => 3, 2 => 2, 1 => 1), Input::old('mark', 5)) }} {{ Form::submit('Comment', array('class' => 'btn btn-success', 'style' => 'clear: both;')) }} </div> {{ Form::close() }} @include('partials.errors', $errors) @endif @foreach($offer->usersComments as $user) <div class="media"> <a class="pull-left" href="#"> <img class="media-object" data-src="holder.js/64x64"> </a> <div class="media-body"> <h4 class="media-heading">{{{ $user->username }}} <span class="label label-success">mark: {{{ $user->pivot->mark }}}</span></h4> <p class="muted">{{ str_replace("\r\n", '<br>', e($user->pivot->body)) }}</p> </div> </div> @endforeach @stop 

Now, under the discounts, users can leave their comments, one by one, and if the user has already left a comment, the form will not be displayed for him.

The next step is to distribute access rights to the site. To begin, let us indicate the relationship between users and roles:

 // app/models/User.php ... public function roles() { return $this->belongsToMany('Role'); } ... 

Next, add user role management to the admin panel:

 // app/routes.php ... Route::group(array('before' => 'admin.auth'), function() { ... Route::resource('users', 'UsersController'); Route::post('upload', array('uses' => 'HomeController@uploadOfferImage')); }); ... // app/views/layouts/scaffold.blade.php ... <li>{{ link_to_route('users.index', 'Users') }}</li> <li class="pull-right">{{ link_to_route('login.logout', 'Logout') }}</li> ... 

Remember that Useryou need to add a connection to the model :

 // app/models/User.php ... public function roles() { return $this->belongsToMany('Role'); } ... 

Create a controller UserController:

 // app/controllers/UsersController.php class UsersController extends BaseController { /** * User Repository * * @var User */ protected $user; public function __construct(User $user) { $this->user = $user; } /** * Display a listing of the resource. * * @return Response */ public function index() { $users = $this->user->all(); return View::make('users.index', compact('users')); } /** * Display the specified resource. * * @param int $id * @return Response */ public function show($id) { $user = $this->user->findOrFail($id); return View::make('users.show', compact('user')); } /** * Show the form for editing the specified resource. * * @param int $id * @return Response */ public function edit($id) { $user = $this->user->findOrFail($id); return View::make('users.edit', compact('user')); } /** * Update the specified resource in storage. * * @param int $id * @return Response */ public function update($id) { $user = $this->user->findOrFail($id); $roles = array(); foreach (explode(', ', Input::get('roles')) as $role_name) { if ($role = Role::where('role', '=', $role_name)->first()) { $roles[] = $role->id; } } $user->roles()->sync($roles); return Redirect::route('users.show', $id); } /** * Remove the specified resource from storage. * * @param int $id * @return Response */ public function destroy($id) { $this->user->findOrFail($id)->delete(); return Redirect::route('users.index'); } } 

Create the folder app / views / users and add 3 files there:

 // app/views/users/index.blade.php @extends('layouts.scaffold') @section('main') <h1>All Users</h1> @if ($users->count()) <table class="table table-striped table-bordered"> <thead> <tr> <th>Username</th> <th>Email</th> <th>Roles</th> </tr> </thead> <tbody> @foreach ($users as $user) <tr> <td>{{{ $user->username }}}</td> <td>{{{ $user->email }}}</td> <td> @foreach($user->roles as $role) <span class="badge">{{{$role->role}}}</span> @endforeach </td> <td>{{ link_to_route('users.edit', 'Edit', array($user->id), array('class' => 'btn btn-info')) }}</td> <td> {{ Form::open(array('method' => 'DELETE', 'route' => array('users.destroy', $user->id))) }} {{ Form::submit('Delete', array('class' => 'btn btn-danger')) }} {{ Form::close() }} </td> </tr> @endforeach </tbody> </table> @else There are no users @endif @stop // app/views/users/show.blade.php @extends('layouts.scaffold') @section('main') <h1>Show User</h1> <p>{{ link_to_route('users.index', 'Return to all users') }}</p> <table class="table table-striped table-bordered"> <thead> <tr> <th>Username</th> <th>Email</th> <th>Roles</th> </tr> </thead> <tbody> <tr> <td>{{{ $user->username }}}</td> <td>{{{ $user->email }}}</td> <td> @foreach($user->roles as $role) <span class="badge">{{{ $role->role }}}</span> @endforeach </td> <td>{{ link_to_route('users.edit', 'Edit', array($user->id), array('class' => 'btn btn-info')) }}</td> <td> {{ Form::open(array('method' => 'DELETE', 'route' => array('users.destroy', $user->id))) }} {{ Form::submit('Delete', array('class' => 'btn btn-danger')) }} {{ Form::close() }} </td> </tr> </tbody> </table> @stop // app/views/users/edit.blade.php @extends('layouts.scaffold') @section('main') <h1>Edit User</h1> {{ Form::model($user, array('method' => 'PATCH', 'route' => array('users.update', $user->id))) }} <ul> <li> {{ Form::label('username', 'Username:') }} {{ Form::text('username', $user->username, array('disabled')) }} </li> <li> {{ Form::label('email', 'Email:') }} {{ Form::text('email', $user->email, array('disabled')) }} </li> <li> {{ Form::label('roles', 'Roles:') }} {{ Form::text('roles', Input::old('roles', implode(', ', array_fetch($user->roles()->get(array('role'))->toArray(), 'role')))) }} </li> <li> {{ Form::submit('Update', array('class' => 'btn btn-info')) }} {{ link_to_route('users.show', 'Cancel', $user->id, array('class' => 'btn')) }} </li> </ul> {{ Form::close() }} @if ($errors->any()) <ul> {{ implode('', $errors->all('<li class="error">:message</li>')) }} </ul> @endif @stop @section('scripts') <script> $(document).ready(function(){ function split( val ) { return val.split( /,\s*/ ); } function extractLast( term ) { return split( term ).pop(); } $( "#roles" ) // don't navigate away from the field on tab when selecting an item .bind( "keydown", function( event ) { if ( event.keyCode === $.ui.keyCode.TAB && $( this ).data( "ui-autocomplete" ).menu.active ) { event.preventDefault(); } }) .autocomplete({ source: function( request, response ) { $.getJSON( "/roles", { term: extractLast( request.term ), }, function( data ) { response($.map(data, function(item) { return { value: item.role } })) } ); }, search: function() { // custom minLength var term = extractLast( this.value ); if ( term.length < 2 ) { return false; } }, focus: function() { // prevent value inserted on focus return false; }, select: function( event, ui ) { console.log(ui); console.log(this); var terms = split( this.value ); // remove the current input terms.pop(); // add the selected item terms.push( ui.item.value ); // add placeholder to get the comma-and-space at the end terms.push( "" ); this.value = terms.join( ", " ); return false; } }); }); </script> @stop 

And also change a little metd indexcontrollerRolesController

  ... public function index() { $roles = $this->role->all(); if (Request::ajax()) { $roles = Role::where('role', 'like', '%'.Input::get('term', '').'%')->get(array('id', 'role')); return $roles; } return View::make('roles.index', compact('roles')); } ... 

Now autocompletion works.

Next, in order for us to have no runs, we will roll back all the migrations and use the excellent tool that is provided to us Laravel- this is DatabaseSeeder . With it, we can fill our database with some configuration, or start / test data. To do this, first create a class UsersTableSeederin the app / database / seeds folder :

 // app/database/seeds/UsersTableSeeder.php class UsersTableSeeder extends Seeder { public function run() { $users = array( array( 'username' => 'habrahabr', 'email' => 'habrahabr@habr.com', 'password' => Hash::make('habr'), 'updated_at' => DB::raw('NOW()'), 'created_at' => DB::raw('NOW()'), ) ); DB::table('users')->insert($users); } } 

The logic is as follows: clear the table, create an array of data and insert it into the database.

Do the same with RolesTableSeeder:

 // app/database/seeds/RolesTableSeeder.php class RolesTableSeeder extends Seeder { public function run() { $roles = array( array( 'role' => 'admin', 'updated_at' => DB::raw('NOW()'), 'created_at' => DB::raw('NOW()') ), array( 'role' => 'manager', 'updated_at' => DB::raw('NOW()'), 'created_at' => DB::raw('NOW()') ), array( 'role' => 'moderator', 'updated_at' => DB::raw('NOW()'), 'created_at' => DB::raw('NOW()') ) ); DB::table('roles')->insert($roles); } } 

Here I also added roles managerand moderator, in order to give users with these roles access to individual resources in the admin panel.

Next, create another class Seeder:

 // app/database/seeds/RoleUserTableSeeder.php class RoleUserTableSeeder extends Seeder { public function run() { // Uncomment the below to wipe the table clean before populating DB::table('role_user')->truncate(); $role_user = array( array('user_id' => 1, 'role_id' => 1) ); // Uncomment the below to run the seeder DB::table('role_user')->insert($role_user); } } 

So we added the role to adminour first user.

To clear the database and fill it with our initial data, first modify the file app / database / seeds / DatabaseSeeder.php as follows:

 // app/database/seeds/DatabaseSeeder class DatabaseSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { Eloquent::unguard(); //         $this->call('UsersTableSeeder'); $this->call('RolesTableSeeder'); $this->call('RoleUserTableSeeder'); } } 

And to accept all changes, run the command via the console (being in the / workspace / php / habr / folder ):

 php artisan migrate:refresh --seed 

migrate:refreshrolls back all the migrations, and then launches them again, and the option --seedwill indicate that you also need to start DatabaseSeeder.

Next, we build the logic on the right. Make changes to the Model User:

 // app/models/User.php ... public function isAdmin() { $admin_role = Role::whereRole('admin')->first(); return $this->roles->contains($admin_role->id); } ... public function isManager() { $manager_role = Role::whereRole('manager')->first(); return $this->roles->contains($manager_role->id) || $this->isAdmin(); } ... public function isModerator() { $admin_role = Role::whereRole('admin')->first(); return $this->roles->contains($admin_role->id) || $this->isAdmin(); } ... public function isRegular() { $roles = array_filter($this->roles->toArray()); return empty($roles); } } 

Next, we will change the route file so that it conforms to the rights to use the site:

 // app/routes.php ... Route::post('offer_{id}', array('before' => 'not_guest|regular_user', 'uses' => 'HomeController@commentOnOffer'))->where('id', '[0-9]+'); ... Route::group(array('before' => 'admin.auth'), function() { Route::get('dashboard', function() { return View::make('dasboard'); }); Route::group(array('before' => 'manager_role_only'), function() { Route::resource('cities', 'CitiesController'); Route::resource('companies', 'CompaniesController'); Route::resource('tags', 'TagsController'); Route::resource('offers', 'OffersController'); Route::post('upload', array('uses' => 'HomeController@uploadOfferImage')); }); Route::resource('comments', 'CommentsController'); Route::group(array('before' => 'manager_role_only'), function() { Route::resource('roles', 'RolesController'); Route::resource('users', 'UsersController'); }); }); Route::when('comments*', 'moderator_role_only'); Route::filter('admin_role_only', function() { if (Auth::user()->isAdmin()) { return Redirect::intended('/')->withMessage('You don\'t have enough permissions to do that.'); } }); Route::filter('manager_role_only', function() { if (!Auth::user()->isManager()) { return Redirect::intended('/')->withMessage('You don\'t have enough permissions to do that.'); } }); Route::filter('moderator_role_only', function() { if (!Auth::user()->isModerator()) { return Redirect::intended('/')->withMessage('YYou don\'t have enough permissions to do that.'); } }); Route::filter('admin.auth', function() { if (Auth::guest()) { return Redirect::to('login'); } }); Route::filter('un_auth', function() { if (!Auth::guest()) { Auth::logout(); } }); Route::filter('not_guest', function(){ if (Auth::guest()) { return Redirect::intended('/')->withInput()->with('message', 'You should be logged in to provide this action.'); } }); Route::filter('regular_user', function(){ if (!Auth::guest()) { if (!Auth::user()->isRegular()) { return Redirect::back()->with('message', 'You cannot do that due to your role.'); } } }); 

As you noticed, I added an additional filter to the commenting route. In this way, no one except regular users of the site will be able to leave comments on discounts.

Also here the route was used Route::when()- this is the so-called pattern filter ( Pattern Filter ). It allows the first parameter to pass a pattern URL, the second - the filter itself, which needs to be applied, and the third parameter it can accept an array of HTTP , to which the filter should be applied.

Change the login()controller method LoginController:

 // app/controllers/LoginController.php ... public function login() { if (Auth::attempt(array('email' => Input::get('email'), 'password' => Input::get('password')), true) || Auth::attempt(array('username' => Input::get('email'), 'password' => Input::get('password')), true)) { if (!Auth::user()->isRegular()) { return Redirect::to('dashboard'); } return Redirect::intended('/'); } return Redirect::back()->withInput(Input::except('password'))->with('message', 'Wrong creadentials!'); } 

Now at the entrance to the site ordinary users will get to the main page, and administrators, moderators and managers in the admin panel.

Let's change a little navigation menu for the administration:

 // app/views/layouts/scaffold.blade.php @if(!Auth::guest()) <ul class="nav nav-pills"> @if(Auth::user()->isManager()) <li>{{ link_to_route('offers.index', 'Offers') }}</li> <li>{{ link_to_route('companies.index', 'Companies') }}</li> <li>{{ link_to_route('tags.index', 'Tags') }}</li> <li>{{ link_to_route('cities.index', 'Cities') }}</li> @endif @if(Auth::user()->isModerator()) <li>{{ link_to_route('comments.index', 'Comments') }}</li> @endif @if(Auth::user()->isAdmin()) <li>{{ link_to_route('roles.index', 'Roles') }}</li> <li>{{ link_to_route('users.index', 'Users') }}</li> @endif <li class="pull-right">{{ link_to_route('login.logout', 'Logout') }}</li> </ul> @endif 

Excellent - now each role will be visible to those resources to which they have access.

Emails

An important aspect for a web application is sending mail.

Laraveluses SwiftMailerto create letters ( Laravel Mail ).

First you need to configure the settings for sending mail. As a demonstration for sending letters, I will use my account on gmail, but you can use essentially any service that provides the ability to send mail from its servers (for example, Postmarkapp ).

Mail Setup:

 // app/config/mail.php ... return array( ... 'driver' => 'smtp', ... 'host' => 'smtp.gmail.com', ... 'port' => 587, ... 'from' => array('address' => 'habrahabr@habr.com', 'name' => 'Habra Offers'), ... 'encryption' => 'tls', ... 'username' => 'mygmailaccount@gmail.com', ... 'password' => 'mypassword', ... 'pretend' => false ); 

The parameter pretendis responsible for whether to send letters. If it is set to true , then the sending of letters will not occur, but sending logs will be saved in the site logs ( app / storage / logs ).

First of all, I want the registration letter to be sent to the user with a greeting, for this we will create a template in the app / views / emails folder :

 // app/views/emails/welcome.blade.php <!DOCTYPE html> <html lang="en-US"> <head> <meta charset="utf-8"> </head> <body> <h1>Welcome to Habra Offers!</h1> <div> We are glad that you are interested in us, {{{ $username }}}! </div> </body> </html> 

Next, change store()our method LoginController:

 // app/controllers/LoginController.php ... $user->save(); Mail::send('emails.welcome', array('username' => $user->username), function($message) use ($user) { $message->to($user->email, $user->username)->subject('Welcome to Habra Offers!'); }); Auth::loginUsingId($user->id); ... 

The Mail class for sending mail uses a method send()that takes three arguments:

But a welcome letter is not the only type of letters we need. What if the user forgot his password and wants to recover it? For this Laravelprovides Password Reminders & Reset .
What we need to do:

 cd /workspace/php/habr php artisan auth:reminders php artisan migrate 

To recover the password, just call Password::remind(array('email' => $email))and a letter with a link to reset the password will be sent.

We will need to create 2 templates:

The function trans()is an auxiliary function that displays the localized string from the configuration. You can look in the app / lang / en / reminders.php folder and see what errors can be displayed. To change the localization to, say, the Russian language, you need to change the locale value from en to ru in the app / config / app.php file and add the app / lang / ru folder in which to recreate files as in the app / lang / en folder .


Next, add 4 routes:

 // app/routes.php ... Route::group(array('before' => 'un_auth'), function() { ... Route::get('password/remind', array('as' => 'password.remind', 'uses' => 'LoginController@showReminderForm')); Route::post('password/remind', array('uses' => 'LoginController@sendReminder')); Route::get('password/reset/{token}', array('as' => 'password.reset', 'uses' => 'LoginController@showResetForm')); Route::post('password/reset/{token}', array('uses' => 'LoginController@resetPassword')); }); ... 

To proceed with the restoration, we also add a link on the login page:

 // app/views/login/index.blade.php ... {{ Form::close() }} <p>{{ link_to_route('password.remind', 'Forgot password?') }}</p> ... 

As well as missing methods in LoginController:

 // app/controllers/LoginController.php ... /** * Show reminder form. * * @return Response */ public function showReminderForm() { return View::make('auth.remind'); } /** * Send reminder email. * * @return Response */ public function sendReminder() { $credentials = array('email' => Input::get('email')); return Password::remind($credentials, function($message, $user) { $message->subject('Password Reminder on Habra Offers'); }); } /** * Show reset password form. * * @return Response */ public function showResetForm($token) { return View::make('auth.reset')->with('token', $token); } /** * Reset password. * * @return Response */ public function resetPassword($token) { $credentials = array('email' => Input::get('email')); return Password::reset($credentials, function($user, $password) { $user->password = Hash::make($password); $user->save(); Auth::loginUsingId($user->id); return Redirect::home()->with('message', 'Your password has been successfully reseted.'); }); } 

Now any user can recover their password.

Add another link to enter and register on the site on the main page:
 // app/views/layouts/main.blade.php ... <a class="brand" href="{{ route('home') }}">Habr Offers</a> <ul class="nav"> <li><a href="{{ route('home') }}">Home</a></li> </ul> <div class="btn-group pull-right"> @if(Auth::guest()) <a href="{{ route('login.index') }}" class="btn">Login</a> <a href="{{ route('login.register') }}" class="btn">Register</a> @else <a href="{{ route('login.logout') }}" class="btn">Logout</a> @endif </div> ... 

In order to limit the output on the pages of only those discounts that have not yet ended, we will need to add another method to the Model Offer:

 // app/controllers/Offer.php ... public function scopeActive($query) { return $query->where('expires', '>', DB::raw('NOW()')); } public function scopeSortLatest($query, $desc = true) { $order = $desc ? 'desc' : 'asc'; return $query->orderBy('created_at', $order); } ... 

Thus, in the method we can HomeController@indexonly change Offer::orderBy('created_at', 'desc')->get()to Offer::active()->sortLatest()->get(). Our newly created method will add the conditions we need to the chain of conditions. Let's do the same for sorting methods by tags, cities and companies.

 // app/controllers/HomeController.php ... public function byTag($name) { ... $offers = $tag->offers()->active()->sortLatest()->get(); ... } 


Pagination

An important aspect is pagination. Yes, of course, you can send requests to the database, receive thousands of response lines, and then push them all onto the page. But this is hardly anyone's approach. Limiting the number of returned results from the database is quite simple - at the end of the query, you need to use the method paginate()instead of get(), or all(). A simple example:

 // app/controllers/HomeController.php ... public function index() { $offers = Offer::active()->sortLatest()->paginate(); ... } ... // app/views/home/index.blade.php ... @if ($offers->count()) {{ $offers->links() }} ... {{ $offers->links() }} @else There are no offers @endif ... 

Thus, only 15 results will be displayed on one page, and there will be page transitions at the bottom. The number of results is easily changeable - just transfer the required number to the method, for example, it paginate(1)will give 1 result per page.

 // app/controllers/HomeController.php ... public function byTag($name) { $tag = Tag::whereName($name)->firstOrFail(); $offers = $tag->offers()->active()->sortLatest()->paginate(); $title = "Offers tagged as: " . $tag->name; return View::make('home.index', compact('offers', 'title')); } ... public function byCity($name) { $city = City::whereName($name)->firstOrFail(); $offers = $city->offersr()->active()->sortLatest()->paginate(); $title = "Offers in: " . $city->name; return View::make('home.index', compact('offers', 'title')); } ... public function byCompany($name) { $company = Company::whereName($name)->firstOrFail(); $offers = $company->offers()->active()->sortLatest()->paginate(); $title = "Offers by: " . $company->name; return View::make('home.index', compact('offers', 'title')); } ... 

There is nothing complicated about it.

For convenience, we will do the same in the admin panel.

 // app/controllers/OffersController ... /** * Display a listing of the resource. * * @return Response */ public function index() { $offers = $this->offer->sortLatest()->paginate(); return View::make('offers.index', compact('offers')); } ... 

The last thing I want to add to the site is the conclusion of the last comments on the pages and the bookmarks of the discounts to which the user has posted comments.

Start by adding comments to the page frame:

 // app/views/layouts/main.blade.php <div class="container"> @if (Session::has('message')) <div class="flash alert"> {{ Session::get('message') }} </div> @endif <div class="row-fluid"> <div class="span3"> <h2>Last Comments</h2> @if (count($comments = Comment::take(5)->get()) > 0) @foreach ($comments as $comment) @include('partials.comment', $comment) @endforeach @else There are no comments yet @endif </div> <div class="span9"> @yield('main') </div> </div> </div> 

And also create the template itself comment:

 // app/views/partials/comment.blade.php <div class="well"> <a href="{{ route('home.offer', $comment->offer_id) }}"> {{ $comment->user->username }} <span class="label label-success pull-right">mark: {{ $comment->mark }}</span> </a> <div>{{ $comment->webBody() }}</div> </div> 

Do not forget to add a link between the Model Comment Userand Offer:

 // app/models/Comment.php ... public function user() { return $this->belongsTo('User'); } public function offer() { return $this->belongsTo('Offer'); } public function webBody($options = array()) { $str = $this->body; if (isset($options['shorten'])) { $length = isset($options['length']) ? (int) $options['length'] : 50; $end = isset($options['end']) ? : '…'; if (mb_strlen($str) > $length) { $str = mb_substr(trim($str), 0, $length); $str = mb_substr($str, 0, mb_strlen($str) - mb_strpos(strrev($str), ' ')); $str = trim($str.$end); } } $str = str_replace("\r\n", '<br>', e($str)); return $str; } ... 

As well as an auxiliary function to reduce and get rid of the html-comment.

It remains to add bookmarks for the user:

 // app/routes.php Route::get('/', array('as' => 'home', 'uses' => 'HomeController@index')); Route::get('bookmarks', array('before' => 'auth', 'as' => 'home.bookmarks', 'uses' => 'HomeController@bookmarks')); ... // app/views/layouts/main.blade.php ... @if(Auth::guest()) <a href="{{ route('login.index') }}" class="btn">Login</a> <a href="{{ route('login.register') }}" class="btn">Register</a> @else <a href="{{ route('home.bookmarks') }}" class="btn">My Bookmarks</a> <a href="{{ route('login.logout') }}" class="btn">Logout</a> @endif ... // app/models/User.php ... public function usersOffers() { return $this->belongsToMany('Offer', 'comments')->withPivot('body', 'mark')->withTimestamps(); } ... // app/controllers/HomeController.php ... /** * Display a listing of bookmarked offers. * * @return Response */ public function bookmarks() { $offers = Auth::user()->usersOffers()->paginate(); $title = "My Bookmarked Offers"; return View::make('home.index', compact('offers', 'title')); } ... 

To begin with, we added a route to app / route.php , then added a link to it in app / views / layouts / main.blade.php , set the connection between Model Userand Offer, and finally implemented the method bookmarksin HomeController.

Depla


The hour of deployment has arrived! For this, I chose fortrabbit.com - hosting for applications on PHP. It supports Git, SSH, Memcached, Composer, MySQLand more.

The registration process there is quite simple.



Next, create a new application.



Let's call him habr. The project name will be a link to it habr.eu1.frbit.net/ . Add a note (Habra Offers), and add a sshkey from your car. To view your sshkey enter in the terminal:

 cat ~/.ssh/id_rsa.pub 




The final step is to wait for the environment configuration. You will generate data for access to the repository Git, SSHand SFTP, MySQLsettings and ReSyncaccess.

The environment is up and running.



fortrabbitfreezes non-active applications. How to unfreeze the application can be read here .
Now, in order to fill our application on fortrabbitgo to the terminal:

 cd && cd workspace/php/ git clone git@git1.eu1.frbit.com:habr.git fort_habr 

A clone of the empty repository will be created with fortrabbit'a. Then simply transfer the entire project from the workspace / php / habr folder to the workspace / php / fort_habr folder . Go to the database configuration file and correct it with new data MySQL. Now we are ready to upload our application:

 cd fort_habr git add . git commit -am "Initial Commit" git push -u origin master 

After all, it remains to go through sshand start the migration. So:

 ssh u-habr@ssh1.eu1.frbit.com 

Then enter your password and you are on the server.
Go to the htdocs folder and execute:

 cd htdocs php artisan migrate:install php artisan migrate --seed 

If the database setting was correct, no problems should arise.

To work with Composeron the hosting, you can not even use it ssh- just add a trigger in the commit:

 git commit --allow-empty -am "Update dependencies [trigger:composer:update]" git push -u origin master 

The option is --allow-emptyhere so that we can run to make a commit without making any changes to the files. As if an empty commit. But after seeing the comments [trigger:composer:update], the hosting will automatically launch the command composer update, and all the dependencies of the project will be updated.

By the way, in my repository on GitHub, I also added seedspictures for discounts.

And the last thing: before going to your website, make sure that in Domainson the server Root Pathmatches the value public. So how exactly is it arranged Laravel.

You can play here: Habra Offers .

Conclusion


I hope you were interested in reading this, and it is useful to do it. Laravel- an excellent framework for developing web applications of varying complexity.

I tried to explain the main, and even more, aspects. And for the interest of giving homework:



Perhaps a good task, as you think?

about the author



Statistics collection



All grammatical errors please write in lichku.

Haters gonna die (I bet that I'll write it).

UPD : Useful links

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


All Articles