Laravel руководство разработчика

В этом руководстве по Laravel для начинающих мы расскажем, как установить Laravel и создать своё первое приложение на нём. В этом руководстве мы простым языком объясним все необходимые понятия, чтобы вы могли легко изучить Laravel и лучше его понять.

  • Введение
  • Установка и настройка
  • Шаги для пользователей Mac:
  • Шаги для пользователей Windows:
  • Руководство по созданию простого CRUD-приложения для составления списка дел на Laravel
    • Изучение структуры папок
    • Давайте разберём некоторые из них
  • Пошаговое создание вашего первого приложения на Laravel
    • Создайте ваш проект
    • Настройте базу данных
    • Создайте аутентификацию
    • Миграции
    • Модели -Eloquent
    • Отношение «один-ко-многим»
    • Команда Tinker в Artisan (необязательно)
    • Контроллеры
    • Маршрутизация
    • Представления – шаблоны Blade
    • Привязка модели к маршруту (Route-Model Binding)
    • Редактируем представления
    • Запускаем проект на Localhost
  • Заключение

Laravel – это элегантный, выразительный и гибкий PHP-фреймворк с упором на чистый код и скорость. Он позиционирует себя как «PHP-фреймворк для веб-мастеров». Это бесплатный PHP-фреймворк с открытым исходным кодом, созданный Тейлором Отвелом на основе архитектурной модели Model View Controller (MVC).

Создание веб-приложения с нуля может быть сложной задачей, особенно если вы новичок. Простое веб-приложение содержит различные маленькие и большие компоненты, и вам, возможно, надоест создавать эти компоненты каждый раз, когда вы разрабатываете приложение. Это скучный и повторяющийся процесс, и нет никакого смысла изобретать велосипед. Вот тогда вам на помощь и приходит фреймворк Laravel.

Введение

Фреймворк Laravel предоставляет различные PHP-библиотеки и вспомогательные функции. Он обеспечивает общие функции и логику, чтобы упростить и ускорить разработку, а также помочь вам сосредоточиться на более важных вещах.

Сначала придется уделить немного времени обучению, особенно если вы новичок и не имеете опыта работы с какими-либо веб-фреймворками. Но поверьте мне, если вы наловчитесь, вы не только полюбите, но и пристраститесь к Laravel. Этот фреймворк поддерживает творчество в разработке. Он использует слово «Веб-мастер», чтобы указать на креатив, скрытый в сердце разработчика. Результат — эффективное приложение с меньшим количеством строк и хорошо продуманным кодом.

Чтобы вам было легче изучить Laravel, я написал это руководство специально для неопытной аудитории. Так вам будет проще следовать этому руководству и изучать Laravel.

Что вы должны знать перед использованием этого руководства по Laravel?

  • HTML/CSS (Естественно).
  • Понимание базового PHP.
  • Владение PHP на среднем уровне – это не обязательно, но если у вас есть время, изучите некоторые рядовые вещи: ООП в PHP, абстракцию и т. д.
  • Базовое понимание фреймворка MVC.
  • Усидчивость – несмотря на то, что изучать Laravel довольно легко, спустя какое-то время вам всё же придется испытать собственное терпение. По крайней мере, у меня были некоторые проблемы из-за того, что я знал PHP, но совсем не разбирался в фреймворках. Когда я изучал фреймворк или успешно завершал проекты, я всё равно путался с базовыми вещами, которые лежат в основе фреймворка MVC. Но я не сдавался.
  • Увлечение – да ладно вам, ведь веб-разработка – это весело! По крайней мере, когда используешь Laravel. Лучше всего получать наслаждение от своего познавательного путешествия.

Laravel предлагает различные способы установки на Windows или Mac. Лучший и самый простой способ установить Laravel – через Composer. Composer — это менеджер зависимостей для PHP, который вы можете установить на свой веб-сервер.

Требования для установки Laravel 8

Перед установкой Laravel на вашу локальную платформу (Localhost) вам необходимо установить следующие программы:

  • Веб-сервер – Apache или nginx
  • >= PHP 7.3
  • Некоторые расширения PHP, которые можно установить заранее:
  1. BCMath
  2. Ctype
  3. Fileinfo
  4. JSON
  5. Mbstring
  6. OpenSSL
  7. PDO
  8. Tokenizer
  9. XML
  • MySQL (или другие системы управления базой данных, вы даже можете использовать SQLite).
  • Composer
  • IDE (интегрированная среда разработки) будет очень полезна для разработки на Laravel. Я рекомендую VS Code или Atom. И то, и другое можно использовать совершенно бесплатно.

Пошаговая установка Laravel на локальном хосте:

  1. Убедитесь, что все компоненты установлены.
  2. Установите Composer, используя следующую команду (если у вас уже установлен и настроен Composer, пропускайте этот шаг).
  3. Запустите Composer, чтобы проверить, успешно ли он установлен глобально, как показано ниже.
  4. Шаги для пользователей Mac:

  5. Теперь установите Laravel Installer (это отличный CLI-инструмент для создания новых проектов в Laravel с множеством параметров конфигурации!), используя следующую команду:
    composer global require “laravel/installer”
  6. Теперь выполните команду, приведенную ниже, чтобы поместить папку Composer / vendor / bin в переменную $ PATH (если у вас уже был установлен и настроен Composer, пропустите этот шаг).
  7. Чтобы создать проект после успешного завершения установки, вам нужно сделать следующее:
  • Перейти в нужную папку;
  • Выполнить команду laravel new projectname.
  1. Скачайте Composer и установите его.
  2. После успешного завершения установки необходимо проверить, установлен ли он глобально. Откройте командную строку и введите команду «Composer», как показано ниже.
  3. Шаги для пользователей Windows:

  4. Поместите путь к папке ~ / .composer / vendor / bin в переменную среды PATH
  5. Теперь выполните следующую команду –
    composer global require “laravel/installer”
  6. Чтобы создать проект после установки, вам нужно сделать следующее:
  • Перейти в нужную папку;
  • Зажать клавишу Shift + щелкнуть правой кнопкой мыши и выбрать пункт «Открыть командную строку здесь»;
  • Выполнить команду laravel new projectname/

В нашем примере выполняем:

Лучший способ изучить программирование – практиковаться. Поэтому здесь мы будем изучать основы Laravel, разрабатывая простое веб-приложение, которое будет выполнять перечисленные ниже функции. В рамках этого руководства по Laravel:

  • Вы сможете зарегистрироваться и войти в веб-приложение;
  • Вы cможете добавлять задачи в свой список дел;
  • Вы cможете редактировать или удалять эти задачи;
  • Ваш список виден только вам, поэтому он использует аутентификацию через электронную почту и пароль.

Laravel — приложения следуют шаблону проектирования архитектуры MVC (Model-View-Controller).

Изучение структуры папок

  • Модели (Models) представляют сущности в базе данных, помогают запрашивать базу данных и возвращать данные.
  • Представления (View) – это страницы, которые будут отображаться при доступе к приложению. Элементы представлений используются для пользовательского интерфейса приложения.
  • Контроллеры (Controllers) обрабатывают запросы пользователей, получают необходимые данные от моделей и передают их представлениям. Контроллеры действуют как посредники между компонентами модели и представления для обработки бизнес-логики, а также входящего запроса.

После того как вы установили Composer и создали свое первое веб-приложение на основе Laravel, вы могли заметить папку приложения с разными файлами и папками внутри. Я знаю, если вы новичок, у вас может возникнуть много вопросов о том, для чего нужны эти папки и т. д. и т. п.

app

    Console

    Exceptions

    Http

    Models

    Providers

bootstrap

config

database

    migrations

    seeds

public

resources

    css

    js
    
    lang  
  
    views

routes

storage

    app

    framework

    logs

tests

vendor
  • App: эта папка является ядром приложения и содержит основной код.
    • Console: эта папка содержит все пользовательские команды Artisan, созданные с помощью make: command
    • Exceptions: эта папка содержит обработчик исключений приложения. Это хорошее место для добавления пользовательских классов исключений, чтобы обработать различные исключения, создаваемые вашим приложением.
    • Http: этот каталог содержит все ваши контроллеры, промежуточное программное обеспечение и запросы.
    • Models: это новая папка, добавленная в версии Laravel 8 для хранения файлов моделей. Раньше модели хранились в папке App, но теперь их можно хранить и в папках App / Models.
    • Providers: эта папка содержит всех поставщиков услуг для вашего приложения. Вы можете узнать больше о поставщиках услуг здесь.
  • Bootstrap: эта папка содержит загрузочную программу фреймворка и файлы конфигурации. Она также содержит папку с файлами кеша, сгенерированными фреймворком.
  • Config: эта папка содержит все файлы конфигурации вашего приложения.
  • Database: эта папка содержит все миграции и начальные данные базы данных. Вы также можете хранить здесь файлы базы данных SQLite.
  • Public: эта папка содержит такие ресурсы, как изображения, JavaScript и CSS-файлы.
  • Resources: эта папка содержит все файлы представления, а также файлы CSS, LESS и SASS. Здесь также находится папка lang для хранения языковых файлов.
  • Routes: эта папка содержит все маршруты приложения, а файл php получает все запросы к вашему приложению, и здесь вы можете перенаправить запросы на соответствующие методы контроллера.
  • Storage: эта папка содержит все шаблоны Blade, файлы сеансов, файлы кеша и другие.
  • Tests: эта папка содержит все тестовые файлы.
  • Vendor: эта папка содержит все Composer-зависимости.

Если вы не создали свой проект в разделе установки, создайте его сейчас, выполнив следующую команду:

Для нашего приложения нам понадобится база данных, поэтому лучше всего создать её в первую очередь. Laravel поддерживает четыре СУБД:

  • MySQL
  • Postgres
  • SQLite
  • SQL Server

В этом примере мы будем пользоваться SQLite, так как её проще настроить и использовать. И вам не придется ничего устанавливать – нужно будет всего лишь создать один пустой файл. Другие СУБД должны быть установлены в вашей системе, только потом их можно настраивать соответствующим образом. Laravel позволяет файлу config/database.phpнастраивать базу данных, но лучше не хранить в нем учетные данные. Вместо этого вы можете использовать файл .env, в котором можно хранить различные типы учетных и других данных.

В корневой папке Laravel по умолчанию находится файл .env.

В этом файле вы найдете код, похожий на следующий:

•	DB_CONNECTION=mysql
•	
•	DB_HOST=127.0.0.1
•	
•	DB_PORT=3306
•	
•	DB_DATABASE=homestead
•	
•	DB_USERNAME=homestead
•	
•	DB_PASSWORD=secret

Замените все шесть строк, приведенные выше на одну строку, указанную ниже, то есть измените значение db_connection на sqlite и удалите остальные строки db, как здесь:

Теперь в папке базы данных создайте файл database.sqlite (это файл базы данных с расширением .sqlite)

Настройте базу данных

Laravel также предоставляет «скелет» для аутентификации. Это означает, что всё, связанное с аутентификацией: вход пользователя, регистрация, потеря пароля, двухфакторная аутентификация и т.д. будет создаваться заранее, если вам это необходимо. Это называется Laravel Jetstream.

Есть два способа добавить Jetstream в ваше новое Laravel-приложение. Если вы еще не создали проект, добавьте флажок

—jet

для новой команды Laravel:

Так как выше мы уже создали проект, вы можете установить его с помощью установочного пакета. Сначала установите пакет Jetstream, используя следующую команду:

composer require laravel/jetstream

Jetstream в Laravel поддерживает два стека ~ Liveware или Inerta. Поскольку мы хотим, чтобы этот проект был простым, давайте воспользуемся Livewire и установим Jetstream с помощью следующей команды:

php artisan jetstream:install livewire

Создайте аутентификацию

Затем запустите «npm install && npm run dev», чтобы создать свои объекты.

На данном этапе мы закончили с настройкой и можем приступать непосредственно к созданию приложения.

Первый шаг в разработке любого приложения – это создание базы данных. Laravel предлагает отличный способ разработки таблиц и схемы базы данных, а также даёт возможность легко переносить их в разные системы, которые называются «Миграции».

Миграции используются, чтобы создавать, изменять, а также делиться схемой базы данных приложения. Они используются с конструктором схем Laravel, чтобы упростить создание схемы базы данных. В создании миграций для вашей базы данных есть много преимуществ. Вы сможете легко перестроить структуру своей базы данных, используя файлы миграции в производственной и любой другой системе.

Ничего страшного, если это объяснение показалось вам непонятным. Поверьте, скоро у вас всё получится. Просто читайте дальше.

Выполните следующую команду:

php artisan make:migration create_tasks_table --create=tasks

Вы найдёте только что созданную миграцию в папке /database/migrations.

Миграции

Теперь давайте добавим еще два столбца в таблицу задач, отредактировав только что созданный файл миграции.

...
public function up()
{
    Schema::create('tasks', function (Blueprint $table) {
	  $table->bigIncrements('id');
	  $table->string('description');
	  $table->integer('user_id')->unsigned()->index();
	  $table->timestamps();
    });
}
..

В новом столбце с именем «description» будет храниться описание задачи, а в столбце с именем «user_id» будет храниться идентификатор пользователя, создавшего задачу. Мы добавили «-> unsigned () -> index ()» после user_if, потому что это внешний ключ из таблицы пользователей.

Теперь мы закончили с созданием схемы базы данных. Чтобы использовать эту схему для создания таблиц в базе данных, выполните следующую команду. Команда migrate обновит изменения, внесенные в схему, в базе данных.

Eloquent – это ORM (система объектно-реляционного отображения) для Laravel, которая позволяет свободно применять active-record для работы с базой данных. Каждая таблица базы данных может иметь соответствующую модель Eloquent. Модель Eloquent представляет объекты базы данных. Она может использоваться для запроса данных, а также для вставки и обновления данных в таблице. Итак, давайте с помощью команды make: model создадим модель для нашей таблицы задач.

php artisan make:model Task

Эта команда создаст модель задачи в папке приложения, как показано ниже.

Модели -Eloquent

Отношения используются для соединения таблиц. Eloquent даёт возможность связать свои модели через отношения Eloquent. Отношение «один ко многим» означает, что одна модель владеет несколькими объемами другой модели. В нашем примере: у одного пользователя может быть много задач, поэтому между таблицей пользователей и таблицей задач существует связь «один ко многим». Отношения Eloquent очень легко определить и использовать. И преимущество заключается в том, что вам вообще не нужно запускать запросы. Eloquent свяжет модели между собой, поэтому вам придется использовать только функции.

Давайте отредактируем модель задачи и модель пользователя, чтобы создать отношения Eloquent.

Модель задачи (файл task.php находится в app/task.php):

...
use AppModelsUser;
class Task extends Model
{    
    public function user()
    {
    	return $this->belongsTo(User::class);
    }

	  
}

Модель пользователя (файл user.php находится в app/user.php):

...
use AppModelsTask;
class User extends Authenticatable
{
    ...
    public function tasks()
    {
    	return $this->hasMany(Task::class);
    }

	  
}

В Laravel существует интерфейс командной строки, известный как Artisan. Artisan содержит различные команды, и среди них – Tinker, которую мы собираемся обсудить. Tinker позволяет вам взаимодействовать со всем вашим Laravel- приложением через окно консоли без необходимости доступа к веб-интерфейсу. Основным преимуществом Tinker является то, что вы можете тестировать отношения, отлаживать данные и получать доступ к Eloquent ORM, задачам, тестам, событиям и т. д. Поэтому мы также будем использовать команду Tinker в нашем руководстве по Laravel. Допустим, вы зарегистрировались в приложении и создали две задачи. Теперь вы проверяете эти задачи прямо в окне консоли, как показано ниже:

$ php artisan tinker
>AppUser::first()->tasks;

Tinker напрямую обращается к базе данных, так что это отличный инструмент для тестирования функций, а также данных.

Контроллеры используются для распределения трафика между представлениями и моделями. Они могут сгруппировать несколько логических схем обработки запросов в один класс. Таким образом, как правило, они получают весь запрос и, исходя из своей логики, перенаправляют или возвращают соответствующие данные. В нашем примере мы уже подходим к интерфейсу, поэтому мы должны создать контроллеры, через которые сможем обрабатывать запросы, поступающие в наше приложение. Выполните следующую команду, чтобы создать контроллер для задач:

php artisan make:controller TasksController

Таким образом, будет создан TasksController, который вы сможете найти в папке app / Http / Controllers.

Маршрутизация означает принятие запроса и его перенаправление к соответствующей функции. Нашему приложению понадобится пять маршрутов, которые будут выполнять следующие действия:

  • Авторизоваться;
  • Зарегистрироваться;
  • Показать список всех наших задач;
  • Добавить новые задачи;
  • Удалить существующие задачи.

Laravel Jetstream добавляет вход и регистрацию, поэтому теперь нам нужно позаботиться только о трех маршрутах.

Laravel предоставляет различные файлы маршрутов внутри папки / routes для разных случаев использования. Например, настройка маршрутизации для API будет находиться в файле «/routes/api.php», а настройка маршрутизации для нашего веб-приложения будет находиться в «/routes/web.php».

Теперь давайте отредактируем файл web.php. Ниже вы увидите отредактированную версию этого файла. Внесите соответствующие изменения:

<?php

use IlluminateSupportFacadesRoute;
use AppHttpControllersTasksController;


/*
|--------------------------------------------------------------------------
| Веб-Маршруты
|--------------------------------------------------------------------------
|
| Здесь вы можете зарегистрировать веб-маршруты для своего приложения. Эти |маршруты загружает RouteServiceProvider внутри группы, которая содержит |группу промежуточного программного обеспечения «web». А теперь создайте что-|нибудь замечательное!
*/

Route::get('/', function () {
    return view('welcome');
});

Route::middleware(['auth:sanctum', 'verified'])->group(function(){
    Route::get('/dashboard',[TasksController::class, 'index'])->name('dashboard');

    Route::get('/task',[TasksController::class, 'add']);
    Route::post('/task',[TasksController::class, 'create']);
    
    Route::get('/task/{task}', [TasksController::class, 'edit']);
    Route::post('/task/{task}', [TasksController::class, 'update']);
});

Здесь мы внесли два изменения:

  1. Мы сгруппировали все маршруты, чтобы можно было применять auth: sanctum и проверенное промежуточное программное обеспечение ко всем маршрутам, тем самым давая доступ к этим страницам только проверенным пользователям, вошедшим в систему.
  2. Мы изменили маршрут для панели управления, который теперь будет передавать запрос в функцию index в TaskController. Также мы создали маршруты для других действий.

Представления хранятся в папке resources/views. Представления — это интерфейс Laravel- приложения, который разделяет логику приложения и логику представления. Нам нужно создать и настроить следующие представления:

  1. blade.php (панель управления покажет список задач);
  2. blade.php (форма, которая позволит вам добавить новую задачу);
  3. blade.php (форма, которая позволит вам отредактировать любую задачу).

Laravel включает в себя довольно удобный макет, который содержит панель навигации под названием app.blade.php, расположенную в папке Views / layouts. С помощью механизма Blade в Laravel вы cможете разделить свои страницы на подсекции, а также использовать раздел панели навигации по умолчанию в новых представлениях.

Теперь в папке / resources / views создайте файлы add.blade.php и edit.blade.php с разметкой, приведенной ниже.

<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            //здесь должен быть header вашей страницы
        </h2>
    </x-slot>

//здесь должно быть содержимое тэга body

</x-app-layout>

В файле dashboard.blade.php также замените весь код на тот, который приведен выше. Представления мы отредактируем позже, после определения функций нашего контроллера с помощью привязки модели к маршруту.

В Laravel есть множество удивительных функций, которые делают веб-разработку простой, чистой и менее трудоемкой. Одна из наиболее заметных функций подобного рода – привязка модели к маршруту (Route-Model Binding). Это механизм для внедрения экземпляра модели в ваши маршруты. Это значит, вы можете передавать объект модели в маршруты, а также в представления по маршрутам. Эта функция поможет вам легко получить значения объекта в представлении. Ничего страшного, если это объяснение кажется непонятным. Со временем вы все поймёте.

Теперь давайте добавим в TasksController.php функции, обрабатывающие указанные выше маршруты. Они должны выглядеть так, как показано ниже:

<?php

namespace AppHttpControllers;

use IlluminateHttpRequest;
use AppModelsTask;

class TasksController extends Controller
{
    public function index()
    {
        $tasks = auth()->user()->tasks();
        return view('dashboard', compact('tasks'));
    }
    public function add()
    {
    	return view('add');
    }

    public function create(Request $request)
    {
        $this->validate($request, [
            'description' => 'required'
        ]);
    	$task = new Task();
    	$task->description = $request->description;
    	$task->user_id = auth()->user()->id;
    	$task->save();
    	return redirect('/dashboard'); 
    }

    public function edit(Task $task)
    {

    	if (auth()->user()->id == $task->user_id)
        {            
                return view('edit', compact('task'));
        }           
        else {
             return redirect('/dashboard');
         }            	
    }

    public function update(Request $request, Task $task)
    {
    	if(isset($_POST['delete'])) {
    		$task->delete();
    		return redirect('/dashboard');
    	}
    	else
    	{
            $this->validate($request, [
                'description' => 'required'
            ]);
    		$task->description = $request->description;
	    	$task->save();
	    	return redirect('/dashboard'); 
    	}    	
    }
}

Как вы можете видеть, я передаю в функцию объект «Task $ task», а также объект «Request $ request» с помощью механизма привязки модели маршрута.

Примечание: Не забудьте добавить «use App Models Task;» иначе вы получите сообщение об ошибке «Класс не найден».

Теперь функции контроллера настроены и возвращают соответствующие представления с прикрепленными объектами модели. Итак, теперь нужно отредактировать наши представления, чтобы они, если нужно, показывали форму и необходимые данные, используя объекты модели, переданные по маршрутам.

Страница, отображающая все задачи

Откройте файл dashboard.blade.php и отредактируйте его следующим образом:

<x-app-layout>
<x-slot name="header">
    <h2 class="font-semibold text-xl text-gray-800 leading-tight">
        {{ __('Dashboard') }}
    </h2>
</x-slot>

<div class="py-12">
    <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
        <div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-5">
            <div class="flex">
                <div class="flex-auto text-2xl mb-4">Tasks List</div>
                
                <div class="flex-auto text-right mt-2">
                    <a href="/task" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Add new Task</a>
                </div>
            </div>
            <table class="w-full text-md rounded mb-4">
                <thead>
                <tr class="border-b">
                    <th class="text-left p-3 px-5">Task</th>
                    <th class="text-left p-3 px-5">Actions</th>
                    <th></th>
                </tr>
                </thead>
                <tbody>
                @foreach(auth()->user()->tasks as $task)
                    <tr class="border-b hover:bg-orange-100">
                        <td class="p-3 px-5">
                            {{$task->description}}
                        </td>
                        <td class="p-3 px-5">
                            
                            <a href="/task/{{$task->id}}" name="edit" class="mr-3 text-sm bg-blue-500 hover:bg-blue-700 text-white py-1 px-2 rounded focus:outline-none focus:shadow-outline">Edit</a>
                            <form action="/task/{{$task->id}}" class="inline-block">
                                <button type="submit" name="delete" formmethod="POST" class="text-sm bg-red-500 hover:bg-red-700 text-white py-1 px-2 rounded focus:outline-none focus:shadow-outline">Delete</button>
                                {{ csrf_field() }}
                            </form>
                        </td>
                    </tr>
                @endforeach
                </tbody>
            </table>
            
        </div>
    </div>
</div>
</x-app-layout>

Совет:

Механизм шаблонов Blade позволяет нам использовать PHP внутри HTML, не заключая его в «<? Php?>».

Страница, добавляющая новую задачу

Откройте файл add.blade.php и отредактируйте его следующим образом:

<x-app-layout>
<x-slot name="header">
    <h2 class="font-semibold text-xl text-gray-800 leading-tight">
        {{ __('Add Task') }}
    </h2>
</x-slot>

<div class="py-12">
    <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
        <div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-5">
        
            <form method="POST" action="/task">

                <div class="form-group">
                    <textarea name="description" class="bg-gray-100 rounded border border-gray-400 leading-normal resize-none w-full h-20 py-2 px-3 font-medium placeholder-gray-700 focus:outline-none focus:bg-white"  placeholder='Enter your task'></textarea>  
                    @if ($errors->has('description'))
                        <span class="text-danger">{{ $errors->first('description') }}</span>
                    @endif
                </div>

                <div class="form-group">
                    <button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Add Task</button>
                </div>
                {{ csrf_field() }}
            </form>
        </div>
    </div>
</div>
</x-app-layout>

Совет:

{{csrf_field ()}} используется для генерации токена csrf и вставки в форму. Этот токен используется для проверки того, что запрос в приложении исходит от авторизованного зарегистрированного пользователя. Это стандартная функция безопасности, которую предоставляет Laravel.

Страница, редактирующая задачу

Откройте файл edit.blade.php и отредактируйте его, как показано ниже:

<x-app-layout>
<x-slot name="header">
    <h2 class="font-semibold text-xl text-gray-800 leading-tight">
        {{ __('Edit Task') }}
    </h2>
</x-slot>

<div class="py-12">
    <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
        <div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-5">
        
            <form method="POST" action="/task/{{ $task->id }}">

                <div class="form-group">
                    <textarea name="description" class="bg-gray-100 rounded border border-gray-400 leading-normal resize-none w-full h-20 py-2 px-3 font-medium placeholder-gray-700 focus:outline-none focus:bg-white">{{$task->description }}</textarea>	
                    @if ($errors->has('description'))
                        <span class="text-danger">{{ $errors->first('description') }}</span>
                    @endif
                </div>

                <div class="form-group">
                    <button type="submit" name="update" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Update task</button>
                </div>
            {{ csrf_field() }}
            </form>
        </div>
    </div>
</div>
</x-app-layout>

После редактирования этого представления мы собираем все части воедино, так что теперь сможем протестировать своё приложение.

Сначала зарегистрируйтесь, затем войдите в систему, а потом проверьте, можете ли вы создать задачу, отредактировать задачу и удалить задачу.

Чтобы запустить проект, запустите в окне терминала команду php artisan serve. Убедитесь, что вы находитесь в корне вашего приложения в терминале.

Обязательно прочтите инструкции, прежде чем копировать код с github.

Что делать с этим проектом дальше:

Существует множество вещей, которые можно добавить в этот проект, например:

  • Проверка формы:
  • Разрешение пользователю создавать несколько списков с несколькими задачами;
  • Профиль пользователя;
  • И многое другое.

В этом длинном руководстве по Laravel мы узнали, как установить Laravel, настроить базу данных, а также такие основные вещи, как маршруты, модели, представления и контроллеры, а также создали ваше первое приложение на Laravel — приложение для составления списка задач.

Надеюсь, что это руководство по Laravel помогло вам понять основы Laravel, а также мотивировало вас учиться дальше. Пожалуйста, поделитесь своим мнением о Laravel и об этом руководстве. А также вы можете делиться в комментариях к этой статье вашими вопросами и проблемами. Будем рады вам помочь. Спасибо.

Опубликовано: четверг, 29 декабря 2022 г. в 09:59

  • Backend
  • Laravel

Если вы никогда не создавали Laravel приложение, позвольте показать вам пошаговое руководство по Laravel с нуля — без особых требований. Следуйте инструкциям, для изучения Laravel.

Laravel рос невероятными темпами с момента своего первого релиза и недавно добавили двух новых штатных сотрудников, которые помогают развивать экосистему фреймворка. Он никуда не денется в ближайшее время, так что мы могли бы попытаться изучить его, верно? Если вы ещё не сделали это. Laravel всегда был ориентирован на разработчиков, уделяя особое внимание опыту разработчиков, производительности и расширяемости. Если спросить любого разработчика Laravel, почему ему нравится Laravel, он почти всегда ответит, что это опыт разработчика. Итак, вопрос в том, зачем писать что-то ещё, когда на Laravel так приятно писать?!

Установка Laravel 9

Это руководство по Laravel предназначено для тех, кто только начал изучать Laravel. Может быть, они знают, что это такое, или пытались установить его пару раз и остановились, потому что почувствовали себя ошеломлёнными. Мы собираемся создать новое приложение Laravel с самого начала, и единственное, что вам нужно, это:

  • Терминал
  • Установленный PHP 8
  • Установленный и доступный в вашей директории Composer
  • Установленный и доступный в вашей директории NPM
  • Установленный SQLite

Так что же мы будем создавать? Мы собираемся сделать хранитель закладок, чтобы вы могли брать ссылки, которые вам интересны и сохранять их. Наряду с этим мы также позволим добавлять тэги к закладкам, для их классификации.

Как начать работу с Laravel? Первое, что нужно сделать, это, конечно же, создать новый проект и сделать это можно несколькими способами: через установщик Laravel, используя Laravel Sail Build или просто с помощью composer create-project. В этом руководстве по Laravel будем использовать composer create-project: хочу, чтобы требования оставались минимальными. Поэтому выберите каталог, в котором вы хотите разместить своё приложение, и выполните команду composer:

composer create-project laravel/laravel bookmarker

Теперь откройте новый каталог bookmarker в вашем редакторе кода, чтобы мы могли начать. Это пустой Laravel проект, наша отправная точка. Не буду делать никаких предположений о том, как вы хотите рассматривать этот проект локально, так как есть много разных вариантов. Вместо этого будем использовать artisan для обслуживания приложения. Запустите следующую команду artisan:

php artisan serve

Вы получите сообщение о том по какому адресу доступно ваше приложение. Откройте его в браузере. Это должен быть экран Laravel по умолчанию. Поздравляю, вы сделали первый шаг с Laravel! Далее мы перейдём к том, как работает это приложение.

Laravel загружает все маршруты из route/web.php, и у вас есть несколько вариантов маршрутизации. Вы можете загрузить представление напрямую, используя Route::view(), когда вам не нужно передавать данные в представление. Вы можете использовать вызов|Замыкание|функцию вызвав Route::get('route', fn () => view('home')) где get — HTTP-метод который вы хотите использовать. Вы также может использовать контроллеры, чтобы изолировать логику внутри одного класса Route::get('route', AppHttpControllersSomeRouteController::class).

По поводу загрузки маршрутов через контроллеры тоже есть варианты. Вы можете объявить их как строки и указать на определённые методы Route::get('route', 'AppHttpControllersSomeController@methodName'). Вы можете объявить ресурсы маршрута, где Laravel примет стандартный Route::resource('route', 'AppHttpControllersSomeController'), который будет сдержать методы index, create, store, show, edit, update и destroy. Они очень подробно объяснены в документации. Вы также можете использовать вызываемые контроллеры, представляющие собой класс с одним методом __invoke(), который обрабатывается как Замыкание Route::get('route', AppHttpControllersSomeController::class).

Подключение базы данных

В этом руководстве по Laravel будем использовать вызываемые контроллеры, так как мне нравиться их использовать. Мне нравиться использовать их, потому что они сохраняют мои маршруты в чистоте, удобны для моей IDE и инкапсулируют каждый маршрут в отдельный класс.

Начиная любой новый проект, вы должны понимать, что вы от него хотите. Как было сказано, мы создаём приложения для хранения закладок, поэтому мы можем предположить, что мы хотим, чтобы наше приложение делало. Напишем несколько требований:

  • Как пользователь, я хочу иметь возможность создавать новые закладки.
  • Как пользователь, я хочу просматривать все свои закладки.
  • Как пользователь, я хочу иметь возможность обновлять или удалять свои закладки.
  • Как пользователь, я хочу иметь возможность кликнуть на одну из своих закладок и посмотреть веб-сайт.
  • Как пользователь, я хочу иметь возможность просматривать закладки, отмеченные определёнными тегами.

Эти требования — почти пользовательские истории, поэтому мы можем их проработать и понять области, которые, возможно, захотим затронуть. Наш первый шаг — проектирование данных, которые нам нужно хранить в базе данных. Мы будем использовать SQLite для хранения данных этого приложения, чтобы наши требования оставались низкими.

Чтобы начать работать с SQLite в нашем Laravel приложении, во-первых, нам нужно создать SQLite файл в database/database.sqlite это можно сделать в терминале или IDE.

Затем нам нужно открыть .env файл и изменить блок database, что бы наше приложение знало о нашей базе данных. Laravel использует env файл для настройки локальной среды, которая будет загружена через различные файлы конфигурации в config/*.php. Каждый файл настраивает определённые части вашего приложения, поэтому не стесняйтесь потратить немного времени на изучение этих файлов и посмотрите как работает конфигурация.

На данный момент в вашем .env файле будет блок который выглядит следующим образом:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=

Теперь нам нужно внести в этот блок следующие изменения. Изменить DB_DATABASE на полный путь к SQLite файлу database/database.sqlite. В DB_CONNECTION указать sqlite и добавить строку DB_FOREIGN_KEYS=true. Остальное можно удалить, у вас должно получиться что-то вроде:

DB_CONNECTION=sqlite
DB_DATABASE=/Users/steve/code/sites/bookmarker/database/database.sqlite
DB_FOREIGN_KEYS=true

Мы установили соединение с базой данных SQLite. Установили путь к файлу базу и указали, что хотим, чтобы SQLite включил внешние ключи.

Когда у нас настроена и сконфигурирована база данных, мы можем запустить миграцию базы данных по умолчанию. В Laravel миграция базы данных используется для обновления состояния базы данных вашего приложения. Каждый раз, когда вы хотите изменить структуру базы данных, вы создаёте новую миграцию для создания таблицы, добавления или удаления полей, или даже полного удаления таблицы. Документация по миграции базы данных превосходна и объясняет все доступные варианты, поэтому когда будет время — обязательно её прочтите. Так же вам стоит прочитать статью Laravel: Все секреты миграции. По умолчанию Laravel поставляется с несколькими миграциями для пользователей, сброса пароля, неудачных заданий и PAT. Они полезны в 99% приложений, поэтому мы оставим их как есть.

К счастью, в Laravel есть готовая модель User, поэтому нам не нужно ничего редактировать или менять. Мы собираем имена пользователей, адреса электронной почты и пароли, сохраняем время создания и обновления пользователя. Итак, у нас есть уже готовая модель данных. Нам нужно подумать о том, как этот пользователь может получить доступ к нашему приложению. Нужно, что бы он мог войти в систему или зарегистрировать новую учётную запись. Для этого в Laravel есть несколько доступных пакетов, или вы можете создать свою собственную аутентификацию. Стандартные пакеты превосходны и настраиваются, поэтому будем использовать их.

Для этого приложения будем использовать Breeze, который представляет базовую основу аутентификации. Но есть и другие варианты, такие как Jetstream позволяющие использовать 2FA и модель команд, в которой могут сотрудничать несколько человек. Существует ещё один пакет под названием Sociality, который позволяет настраивать вход через социальные сети от множества поставщиков. Однако нам это не нужно, поэтому становите Laravel Breeze с помощью следующей команды:

composer require laravel/breeze --dev

Это делает Laravel Breeze зависимостью разработки для приложения, и это зависимость разработки, потому что её нужно установить. После установки, пакет копирует файлы в ваше приложение для маршрутизации, представлений, контроллеров и прочего. Итак, давайте установим пакет, используя следующую команду artisan:

php artisan breeze:install

Наконец, нам нужно установить и собрать фронтенд ресурсы с помощью npm:

npm install && npm run dev

Этот процесс займёт некоторое время, так как необходимо загрузить все пакеты JavaScript или CSS, затем запустить процесс сборки. Как только это будет сделано скрипт завершит свою работу.

Всё установлено и готово. Нужно запустить миграцию базы данных, что бы наша база находилась в определённом состоянии, с которым мы могли бы начать работать. Вы можете сделать это выполнив команду artisan:

php artisan migrate

Эта команда выполнит каждую миграцию, которая содержится в каталоге database/migrations, и применит их к базе данных. Таким образом, состояние базы данных можно напрямую связать с системой управления версиями, что сделает ваше приложение более умным и устойчивым.

Давайте на минуту задумаемся о том, как мы хотим хранить закладки. Каждая закладка должна принадлежать пользователю, иметь уникальный идентификатор, URL-адрес, который можно посетить, и необязательно описание на случай, если вы хотите написать заметку о том, для чего она нужна.

Создание моделей Eloquent

Теперь мы можем сгенерировать новую Eloquent модель и выполнить миграцию с помощью командной строки artisan. Выполните в терминале следующее:

php artisan make:model Bookmark -m

Мы говорим Laravel создать новую модель Eloquent с названием Bookmark, а флаг -m указывает также сгенерировать миграцию. Если вам когда-нибудь понадобится создать новую модель и выполнить миграцию, рекомендуется использовать этот подход, поскольку он выполняет и то, и другое одновременно. Вы так же можете добавить к этой команде другие флаги для создания фабрик моделей, наполнителей и т.д., но мы не будем использовать их в этом руководстве по Laravel.

Это создаст новую миграцию внутри database/migrations, у неё будет временная метка, за которой следует create_bookmarks_table. Откройте её в своей IDE, что бы мы могли структурировать данные. В методе up замените содержимое следующим блоком:

Schema::create('bookmarks', static function (Blueprint $table): void {
$table->id();

$table->string('name');
$table->string('url');
$table->text('description')->nullable();

$table->foreignId('user_id')
->index()->constrained()->cascadeOnDelete();

$table->timestamps();
});

Из приведённого выше фрагмента кода вы можете увидеть описательный характер миграции базы данных, то, как мы создадим новую таблицу, и описание того, как мы хотим, чтобы она была создана. Теперь мы можем применить эти изменения к нашей базе данных, вновь запустив команду миграции artisan:

php artisan migrate

Затем, давайте перейдём к нашей Eloquent Модели и добавим код, что бы она знала о полях базы данных и любых отношениях, которые они могут иметь. Откройте app/Models/Bookmark.php и замените содержимое следующим кодом:

declare(strict_types=1);

namespace AppModels;

use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;
use IlluminateDatabaseEloquentRelationsBelongsTo;

class Bookmark extends Model
{
use HasFactory;

protected $fillable = [
'name',
'url',
'description',
'user_id'
];

public function user(): BelongsTo
{
return $this->belongsTo(
related: User::class,
foreignKey: 'user_id',
);
}
}

Мы установили fillable атрибуты в соответствии с полями, доступными в таблице. Это остановит любые проблемы с массовым назначением атрибутов. Затем мы добавили метод user который является отношением. Запись Bookmark BelongsTo User (буквально: Закладка принадлежит Пользователю), использующему внешний ключ user_id. Мы можем добавить отношение к нашей модели User, поэтому в каталоге Models откройте файл User.php и замените содержимое следующим кодом:

declare(strict_types=1);

namespace AppModels;

use IlluminateContractsAuthMustVerifyEmail;
use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentRelationsHasMany;
use IlluminateFoundationAuthUser as Authenticatable;
use IlluminateNotificationsNotifiable;
use LaravelSanctumHasApiTokens;

class User extends Authenticatable
{
use Notifiable;
use HasFactory;
use HasApiTokens;

protected $fillable = [
'name',
'email',
'password',
];

protected $hidden = [
'password',
'remember_token',
];

protected $casts = [
'email_verified_at' => 'datetime',
];

public function bookmarks(): HasMany
{
return $this->hasMany(
related: Bookmark::class,
foreignKey: 'user_id',
);
}
}

Теперь пользователь знает об отношениях к закладкам как User HasMany Bookmarks (буквально: у Пользователя Много Закладок). Мы будем использовать эти отношения чуть позже, когда начнём строить логику в нашем приложении.

Наконец, мы можем создать модель Тэг. Мы хотим, что бы с каждой закладкой было связано много тэгов tags. Возьмём в качестве примера Laravel News. Мы могли бы поставить следующие тэги:

  • Laravel
  • News
  • Tutorials
  • Jobs

Таким образом, каждый раз когда мы хотим посмотреть закладки отмеченные любым из этих тэгов, должна появиться закладка Laravel News. Как и раньше, мы собираемся запустить команду artisan для создания модели Tag:

php artisan make:model Tag -m

Теперь откройте файл миграции в текстовом редакторе и снова замените содержимое метода up:

Schema::create('tags', static function (Blueprint $table): void {
$table->id();
$table->string('name');
$table->string('slug')->unique();
});

У наших тэгов есть name и slug, на этот раз нам не нужны временные метки, так как это не важная информация. Я называю это мета моделью, используемой для категоризации и, в основном, системой, пользователь создаёт их, но они не в центре внимания.

Итак, давайте поработаем над Eloquent моделью Tag:

declare(strict_types=1);

namespace AppModels;

use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;

class Tag extends Model
{
use HasFactory;

protected $fillable = [
'name',
'slug',
];

public $timestamps = false;
}

Пока никаких отношений не настраиваем, нам понадобится сводная таблица, для связывания тэгов с закладками. Запустите команду генерации миграции в терминале:

php artisan make:migration create_bookmark_tag_table

В Laravel есть соглашение, согласно которому для названия для сводных таблиц вы задаёте имя указав связываемые таблицы в алфавитном порядке и единственном числе. Итак, мы хотим объединить таблицы bookmarks и tags, поэтому мы называем сводную таблицу bookmark_tag поскольку теги могут принадлежать множеству разных закладок, а закладки могут иметь много тегов.

Давайте заполним эту миграцию, чтобы увидеть, чем она отличается, снова сосредоточившись на методе up:

Schema::create('bookmark_tag', static function (Blueprint $table): void {
$table->foreignId('bookmark_id')->index()->constrained()->cascadeOnDelete();
$table->foreignId('tag_id')->index()->constrained()->cascadeOnDelete();
});

Эта таблица должна содержать внешние ключи для закладок и первичные ключи тэгов. Теперь у нас есть Eloquent модель для этой таблицы, поэтому мы добавляем отношения в модель ‘Tag’ и Bookmark.

Теперь ваша Модель Tag должна выглядеть следующим образом:

declare(strict_types=1);

namespace AppModels;

use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;
use IlluminateDatabaseEloquentRelationsBelongsToMany;

class Tag extends Model
{
use HasFactory;

protected $fillable = [
'name',
'slug',
];

public $timestamps = false;

public function bookmarks(): BelongsToMany
{
return $this->belongsToMany(
related: Bookmark::class,
table: 'bookmark_tag',
);
}
}

Ваша Модель Bookmark должна выглядеть так:

declare(strict_types=1);

namespace AppModels;

use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;
use IlluminateDatabaseEloquentRelationsBelongsTo;
use IlluminateDatabaseEloquentRelationsBelongsToMany;

class Bookmark extends Model
{
use HasFactory;

protected $fillable = [
'name',
'url',
'description',
'user_id'
];

public function user(): BelongsTo
{
return $this->belongsTo(
related : User::class,
foreignKey: 'user_id',
);
}

public function tags(): BelongsToMany
{
return $this->belongsToMany(
related: Tag::class,
table: 'bookmark_tag',
);
}
}

Наконец выполните миграцию для обновления состояния базы данных:

php artisan migrate

Создание пользовательского интерфейса

Теперь, когда наши модели Bookmark и Tag знакомы друг с другом, мы можем приступить к созданию пользовательского интерфейса! Мы не будем сосредотачиваться на отточенном пользовательском интерфейсе, поэтому не стесняйтесь проявлять творческий подход. Однако мы будем использовать tailwindcss.

Мы собираемся сделать большую часть нашей работы для закладок в маршруте панели управления (dashboard), созданном Laravel Breeze, поэтому, если вы посмотрите в routes/web.php вы должны увидеть следующее:

declare(strict_types=1);

use IlluminateSupportFacadesRoute;

Route::get('/', function () {
return view('welcome');
});

Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth'])->name('dashboard');

require __DIR__.'/auth.php';

Маршрут панели управления в настоящее время замыкание, и мы, вероятно, хотим преобразовать его в контроллер. Итак, давайте создадим новый контроллер, чтобы сохранить логику для панели управления, запустив следующую команду artisan:

php artisan make:controller DashboardController --invokable

Теперь давайте реорганизуем файл маршрутов, чтобы он стал немного чище:

declare(strict_types=1);

use IlluminateSupportFacadesRoute;

Route::view('/', 'welcome')->name('home');

Route::get(
'/dashboard',
AppHttpControllersDashboardController::class
)->middleware(['auth'])->name('dashboard');

require __DIR__.'/auth.php';

Мы упростили маршрут home до view маршрута, а маршрут dashboard теперь указывает на контроллер. Откройте этот контроллер в редакторе, чтобы можно было вставить логику из приведённого ниже кода:

declare(strict_types=1);

namespace AppHttpControllers;

use AppModelsBookmark;
use IlluminateContractsViewView;
use IlluminateHttpRequest;

class DashboardController extends Controller
{
public function __invoke(Request $request): View
{
return view('dashboard', [
'bookmarks' => Bookmark::query()
->where('user_id', auth()->id())
->get()
]);
}
}

Как и раньше, всё что нам нужно сделать это прямо сейчас вернуть представление. Теперь давайте проверим это, запустив следующую команду artisan, для запуска вашего приложения:

php artisan serve

Теперь, если вы откроете своё приложение в браузере, вверху справа вы должны увидеть две ссылки Login и Register (Вход и Регистрация). Попробуйте зарегистрировать учётную запись и подождите пока она перенаправит в панель управления. Вы должны увидеть сообщение You’re logged in!Вы вошли!.

Фантастическая работа продолжается! У вас есть приложение Laravel обрабатывающее аутентификацию и модели данных в фоновом режиме, которые можно использовать для создания закладок и управления ими.

Laravel blade

Когда дело доходит до фронтэнда, могут появиться затруднения в выборе, так как существуют миллионы библиотек на JavaScript, есть обычный PHP и Blade, которые вы можете использовать. В этой части руководства мы сосредоточимся на использовании Laravel Blade, поскольку не хотим лишних сложностей или дополнительных пакетов на раннем этапе обучения.

Когда мы установили Laravel Breeze, мы получили несколько дополнительных файлов представления, и это здорово, поскольку он уже настроен. Для новой формы закладок я создам новый компонент Blade, который представляет собой отдельное представление view, которое мы можем использовать в нескольких местах.

Создайте анонимный компонент Blade, который является просто файлом представления, выполнив следующую команду:

php artisan make:component bookmarks.form --view

Затем внутри нашего resources/views/dashboard.blade.php проведём рефакторинг, что бы он выглядел следующим образом:

<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Dashboard') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<x-bookmarks.form :bookmarks="$bookmarks" />
</div>
</div>
</div>
</x-app-layout>

Мы загружаем blade компонент, вызывая <x-bookmarks.form />, и вот как это работает: Все blade компоненты можно загружать добавляя к имени префикс x-. Если он расположен в подкаталоге, мы обозначаем каждый подкаталог точкой ., поэтому глядя на x-bookmarks.form, мы можем предположить, что он хранится в resources/views/components/bookmarks/form.blade.php. В нём мы собираемся сделать простой способ добавления новых закладок. Добавьте следующий (массивный) фрагмент кода внутрь компонента:

@props(['bookmarks'])
<div>
<div x-data="{ open: true }" class="overflow-hidden">
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<div class="-ml-4 -mt-2 flex items-center justify-between flex-wrap sm:flex-nowrap">
<div class="ml-4 mt-2">
<h3 class="text-lg leading-6 font-medium text-gray-900">
Your Bookmarks
</h3>
</div>
<div class="ml-4 mt-2 flex-shrink-0">
<a x-on:click.prevent="open = ! open" class="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<span x-show="! open" x-cloak>Show Form</span>
<span x-show="open" x-cloak>Hide Form</span>
</a>
</div>
</div>
</div>
<div x-show="open" x-cloak class="divide-y divide-gray-200 py-4 px-4">
<div class="pt-8">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
Create a new bookmark.
</h3>
<p class="mt-1 text-sm text-gray-500">
Add information about the bookmark to make it easier to understand later.
</p>
</div>
<form id="bookmark_form" method="POST" action="{{ route('bookmarks.store') }}" class="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
@csrf
<div class="sm:col-span-3">
<label for="name" class="block text-sm font-medium text-gray-700">
Name
</label>
<div class="mt-1">
<input type="text" name="name" id="name" class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md">
</div>
@error('name')
<p class="mt-2 text-sm text-red-500">
{{ $message }}
</p>
@enderror
</div>
<div class="sm:col-span-3">
<label for="url" class="block text-sm font-medium text-gray-700">
URL
</label>
<div class="mt-1">
<input type="text" name="url" id="url" class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md">
</div>
@error('url')
<p class="mt-2 text-sm text-red-500">
{{ $message }}
</p>
@enderror
</div>
<div class="sm:col-span-6">
<label for="description" class="block text-sm font-medium text-gray-700">
Description
</label>
<div class="mt-1">
<textarea id="description" name="description" rows="3" class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border border-gray-300 rounded-md"></textarea>
</div>
<p class="mt-2 text-sm text-gray-500">
Write any notes about this bookmark.
</p>
@error('description')
<p class="mt-2 text-sm text-red-500">
{{ $message }}
</p>
@enderror
</div>
<div class="sm:col-span-6">
<label for="tags" class="block text-sm font-medium text-gray-700">
Tags
</label>
<div class="mt-1">
<input
type="text"
name="tags"
id="tags"
class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md"
/>
<p class="mt-2 text-sm text-gray-500">
Add a comma separated list of tags.
</p>
@error('tag')
<p class="mt-2 text-sm text-red-500">
{{ $message }}
</p>
@enderror
</div>
</div>
<div class="sm:col-span-6">
<div class="pt-5">
<div class="flex justify-end">
<a x-on:click.prevent="document.getElementById('bookmark_form').reset();" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Cancel
</a>
<button type="submit" class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Save
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
@forelse ($bookmarks as $bookmark)
<div>
<a href="#" class="block hover:bg-gray-50">
<div class="px-4 py-4 sm:px-6">
<div class="flex items-center justify-between">
<p class="text-sm font-medium text-indigo-600 truncate">
{{ $bookmark->name }}
</p>
</div>
<div class="mt-2 sm:flex sm:justify-between">
<div class="flex space-x-4">
@foreach ($bookmark->tags as $tag)
<p class="flex items-center text-sm text-gray-500">
{{ $tag->name }}
</p>
@endforeach
</div>
</div>
</div>
</a>
</div>
@empty
<a href="#" class="relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"></path></svg>
<span class="mt-2 block text-sm font-medium text-gray-900">
Create a new bookmark
</span>
</a>
@endforelse
</div>

У нас получился довольно большой компонент, который будет обрабатывать всю логику необходимую для создания закладки во фронтэнде. Начнём с того, что наш компонент объявляет какие атрибуты будут рассматриваться как переменные данных @props(['bookmarks']), которые может использовать компонент. Это просто переменные которые мы передаём, чтобы наш компонент знал о них. Затем у нас есть секция вверху, которая является секцией управления у неё есть заголовок и кнопка действия. Мы используем [Alpine.js](https://alpinejs.dev/) для базового JavaScript, который нам нужен — переключение видимости формы. Наша форма — стандартная HTML-форма, но мы отправляем её данные по маршруту, который нам ещё предстоит создать, мы скоро его добавим. Затем мы добавляем в форму новую blade директиву @csrf, эта директива предотвращает CSRF с которыми мы можем столкнуться, если другие сайты попытаются вмешаться и взломать нашу форму. Остальной код — просто разметка визуальных элементов, поэтому не стесняйтесь настраивать его по своему усмотрению. Следует отметить, что в настоящее время мы добавляем тэги используя список тэгов разделённый запятыми. Мы могли бы подойти к этому немного по другому, если бы использовали больше JavaScript или UI-библиотек. Далее у нас идут кнопки отмены и сохранения. Кнопка отмены — сбрасывает форму с помощью JavaScript, а кнопка отправки, как вы можете догадаться, отправляет форму.

Создание контроллера

Итак, теперь мы должны сохранить данные формы, скорее всего, ваша страница не загрузится, потому что маршрут ещё не определён — и это нормально. Мы собираемся его создать. Однако сначала нам нужно создать контроллер, в который мы собираемся сохранить эти данные. С помощью следующей команды artisan создайте новый контроллер:

php artisan make:controller Bookmarks/StoreController --invokable

Затем добавим в файл маршрутов следующее:

Route::post(
'bookmarks',
AppHttpControllersBookmarksStoreController::class,
)->middleware(['auth'])->name('bookmarks.store');

Теперь внутри контроллера нужно сделать несколько вещей, Во-первых, мы хотим проверить запрос, чтобы мы могли передать информацию обратно с сообщением валидации. Затем мы хотим выполнить действие для создания новой закладки, и наконец, перенаправить пользователя обратно в панель управления, где он увидит только что созданную закладку.

Внутри app/Http/Controllers/Bookmarks/StoreController.php добавим следующий код:

declare(strict_types=1);
namespace AppHttpControllersBookmarks;
use AppHttpControllersController;
use AppModelsTag;
use IlluminateHttpRequest;
use IlluminateHttpRedirectResponse;
class StoreController extends Controller
{
public function __invoke(Request $request): RedirectResponse
{
$this->validate($request, [
'name' => [
'required',
'string',
'min:1',
'max:255',
],
'url' => [
'required',
'url',
],
'description' => [
'nullable',
'string',
],
'tags' => [
'nullable',
'array',
]
]);
$bookmark = auth()->user()->bookmarks()->create([
'name' => $request->get('name'),
'url' => $request->get('url'),
'description' => $request->get('description'),
]);
foreach (explode(',', $request->get('tags')) as $tag) {
$tag = Tag::query()->firstOrCreate(
['name' => trim(strtolower($tag))],
);
$bookmark->tags()->attach($tag->id);
}
return redirect()->route('dashboard');
}
}

У нас есть метод __invoke, который примет текущий запрос. Он обрабатывается Laravel DI контейнером, так что вам не о чем беспокоиться. Основная причина, по которой мы вызываем $this->validate заключается в том, что мы расширяем основной Контроллер для нашего Laravel приложения. Установим правила валидации. Первый аргумент, передаваемый для валидации — данные, которые мы хотим проверить. Затем мы передаём массив правил валидации, которым нужно следовать. Я установил правила в соответствии с разумными значениями по умолчанию и не стеснялся использовать доступные опции валидации из документации Laravel.

Затем мы переходим к созданию нашей закладки. Мы не используем модель, так как можем сэкономить время, получая аутентифицированного пользователя, получая отношения закладок и вызывая create — — значит нам не нужно передавать user_id, так как он доступен непосредственно из auth()->user(). Затем мы циклически перебираем теги из запроса и либо получаем первый соответствующий, либо создаём новый по введённому имени (из которого мы удаляем лишние пробелы и преобразуем в нижний регистр для согласованности). Затем, мы прикрепляем этот новый тег к закладке. Наконец, мы возвращаем redirect()->route('dashboard'), чтобы перенаправить пользователя в панель управления с только что созданной закладкой.

Рефакторинг кода контроллера

Код хорош и делает именно то, что нам нужно, но можем ли мы его улучшить? Я думаю да.

Выносим валидацию в запрос формы

Первый шаг в рефакторинге кода — вынести валидацию из контроллера. Нет ничего плохого в том, чтобы хранить её в контроллере. Однако мы можем сэкономить немного времени, не создавая экземпляр контроллера в случае сбоя проверки. Для этого мы можем создать новый запрос формы, используя следующую команду artisan:

php artisan make:request Bookmarks/StoreRequest

Это создаст новый класс в app/Http/Requests/Bookmarks/StoreRequest.php. Давайте откроем его, добавим код и пройдёмся по нему:

declare(strict_types=1);
namespace AppHttpRequestsBookmarks;
use IlluminateFoundationHttpFormRequest;
class StoreRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => [
'required',
'string',
'min:1',
'max:255',
],
'url' => [
'required',
'url',
],
'description' => [
'nullable',
'string',
],
'tags' => [
'nullable',
'array',
]
];
}
}

Мы используем метод authorize() для проверки является ли авторизированным запрос. На данный момент это нормально, но если вы добавите уровень ролей и разрешений, позже вы сможете гарантировать, что аутентифицированному пользователю разрешено выполнять действие store в закладках. Затем у нас есть метод rules, массив правил проверки, подобный тому, как был у нас в контроллере. Что Laravel будет делать сейчас, используя контейнер DI, когда приходит запрос — прежде чем он создаст экземпляр нового контроллера, он попытается создать запрос формы. Это проверит запрос. Если валидация не пройдёт, будет выдано исключение, которое Laravel поймает, преобразует для вас в ErrorBagи вернутся к предыдущему представлению с этим пакетом ошибок, доступным для отображения любых ошибок валидации. Очень полезная функция Laravel. Но, прежде чем это произойдёт, нам нужно указать нашему контроллеру использовать новый запрос формы, поэтому изменим сигнатуру метода __invoke, следующим образом:

public function __invoke(StoreRequest $request): RedirectResponse

Неудачная валидация нас не затронет. Поэтому мы можем удалить расширение базового контроллера Laravel и удалить ручную проверку:

declare(strict_types=1);
namespace AppHttpControllersBookmarks;
use AppHttpRequestsBookmarksStoreRequest;
use AppModelsTag;
use IlluminateHttpRedirectResponse;
class StoreController
{
public function __invoke(StoreRequest $request): RedirectResponse
{
$bookmark = auth()->user()->bookmarks()->create([
'name' => $request->get('name'),
'url' => $request->get('url'),
'description' => $request->get('description'),
]);
foreach (explode(',', $request->get('tags')) as $tag) {
$tag = Tag::query()->firstOrCreate(
['name' => trim(strtolower($tag))],
);
$bookmark->tags()->attach($tag->id);
}
return redirect()->route('dashboard');
}
}

Внезапно наш контроллер стал намного меньше и его легче понять. Если вам нужно добавить примечания в комментариях, чтобы напомнить о выполнении валидации, не стесняйтесь, пока это не войдёт в рабочий процесс, в котором вы будете помнить что здесь происходит.

Выносим логику в Action

Мы могли бы оставить всё как есть, так как это разумное решение, но стандарт Laravel состоит в том, чтобы переместить логику создания в новый класс, называемый Action.

Не существует специальной команды для создания Action, нужно сделать это вручную. Создайте в каталоге app/Actions/Bookmarks/ новый файл CreateBookmarkAndTags.php.

Затем откройте этот файл в редакторе и добавьте следующий код:

declare(strict_types=1);
namespace AppActionsBookmarks;
use AppModelsBookmark;
use AppModelsTag;
class CreateBookmarkAndTags
{
public function handle(array $request, int $id): void
{
$bookmark = Bookmark::query()->create([
'name' => $request['name'],
'url' => $request['url'],
'description' => $request['description'],
'user_id' => $id,
]);
if ($request['tags'] !== null) {
foreach (explode(',', $request['tags']) as $tag) {
$tag = Tag::query()->firstOrCreate(
['name' => trim(strtolower($tag))],
);
$bookmark->tags()->attach($tag->id);
}
}
}
}

У нас есть один метод handle() принимающий данные запроса и идентификатор, который мы будем использовать в качестве идентификатора пользователя, затем копируем логику из контроллера и вносим несколько незначительных изменений. Мы можем использовать этот action класс в любом месте нашего приложения, из пользовательского интерфейса, из командной строки, или даже API, если это необходимо. Мы создали модульно действие, которое можно легко вызвать, протестировать и получить предсказуемые результаты.

Итак, теперь мы можем ещё больше облегчить контроллер:

declare(strict_types=1);
namespace AppHttpControllersBookmarks;
use AppActionsBookmarksCreateBookmarkAndTags;
use AppHttpRequestsBookmarksStoreRequest;
use IlluminateHttpRedirectResponse;
class StoreController
{
public function __invoke(StoreRequest $request): RedirectResponse
{
(new CreateBookmarkAndTags())->handle(
request: $request->all(),
id: auth()->id(),
);
return redirect()->route('dashboard');
}
}

Теперь у нас есть один action класс, который мы вызываем внутри контроллера, а затем возвращаем перенаправление. Намного чище и с хорошим названием. Конечно, мы можем пойти дальше, если захотим. Используя контейнер Laravel для внедрения action в конструктор — это позволит нам вызывать action класс. Это будет выглядеть следующим образом.

declare(strict_types=1);
namespace AppHttpControllersBookmarks;
use AppActionsBookmarksCreateBookmarkAndTags;
use AppHttpRequestsBookmarksStoreRequest;
use IlluminateHttpRedirectResponse;
class StoreController
{
public function __construct(
protected CreateBookmarkAndTags $action,
) {}
public function __invoke(StoreRequest $request): RedirectResponse
{
$this->action->handle(
request: $request->all(),
id: auth()->id(),
);
return redirect()->route('dashboard');
}
}

Этот последний метод полезен, если у вашего action класса есть требования к его конструктору. Скажем вы используете шаблон Репозитория или другой шаблон — вы можете добавить его в конструктор своего action и Laravel решит это автоматически, если сможет.

Создание контроллера удаления закладок

Таким образом, мы можем показать список и добавить закладки, и мы можем добавить в список кнопку удаления — не смысла создать что-то слишком больше, верно?

Создадим новый контроллер следующей командой:

php artisan make:controller Bookmarks/DeleteController --invokable

Теперь нам не нужен action, так как это однострочное действие, но если вы хотите предоставить несколько способов удаления закладок, выполните те же действия, что и выше, но на этот раз удалите закладку, а не создайте её. Добавьте следующий код в контроллер:

declare(strict_types=1);
namespace AppHttpControllersBookmarks;
use AppModelsBookmark;
use IlluminateHttpRedirectResponse;
use IlluminateHttpRequest;
class DeleteController
{
public function __invoke(Request $request, Bookmark $bookmark): RedirectResponse
{
$bookmark->delete();
return redirect()->route('dashboard');
}
}

Здесь мы принимаем модель Bookmark в качестве аргумента, чтобы мы могли включить привязку модели маршрута, где Laravel будет искать запись для вас и внедрять её в ваш метод — в случае сбоя он выдаст исключение 404. Всё что нам нужно сделать, это вызвать удаление модели и вернуть перенаправление. Добавьте следующий маршрут:

Route::delete(
'bookmarks/{bookmark}',
AppHttpControllersBookmarksDeleteController::class,
)->middleware(['auth'])->name('bookmarks.delete');

Наконец, мы можем вернуться к нашему компоненту и добавить кнопку:

@forelse ($bookmarks as $bookmark)
<div>
<a href="#" class="block hover:bg-gray-50">
<div class="px-4 py-4 sm:px-6">
<div class="flex items-center justify-between">
<p class="text-sm font-medium text-indigo-600 truncate">
{{ $bookmark->name }}
</p>
<div class="ml-2 flex-shrink-0 flex">
<form method="DELETE" action="{{ route('bookmarks.delete', $bookmark->id) }}">
@csrf
<button type="submit" class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-red-500 bg-gray-100 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Delete
</button>
</form>
</div>
</div>
<div class="mt-2 sm:flex sm:justify-between">
<div class="flex space-x-4">
@foreach ($bookmark->tags as $tag)
<p class="flex items-center text-sm text-gray-500">
{{ $tag->name }}
</p>
@endforeach
</div>
</div>
</div>
</a>
</div>
@empty
<a href="#" class="relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"></path></svg>
<span class="mt-2 block text-sm font-medium text-gray-900">
Create a new bookmark
</span>
</a>
@endforelse

Добавляем контроллера перехода по закладке

Теперь мы всё связали. Мы можем создать список и удалить. Последнее, что я сделаю, это добавлю способ просмотра закладок в представление. Простым способом сделать это было бы добавить кнопку, позволяющую открывать ссылку в новой вкладке, но это было бы скучно…

Вместо этого я буду перенаправлять закладку с реферером, что бы люди могли отслеживать, откуда она исходит. Для этого снова создадим контроллер с помощью консольной команды artisan:

php artisan make:controller Bookmarks/RedirectController --invokable

Добавим GET маршрут:

Route::get(
'bookmarks/{bookmark}',
AppHttpControllersBookmarksRedirectController::class
)->middleware(['auth'])->name('bookmarks.redirect');

Управление созданием этого URL-адреса мы могли бы написать вручную. Однако я создал библиотеку для таких ситуаций под названием juststeveking/uri-builder, которая позволит мне создавать URI и свободно добавлять дополнительные части.

declare(strict_types=1);
namespace AppHttpControllersBookmarks;
use AppHttpControllersController;
use AppModelsBookmark;
use IlluminateHttpRequest;
use JustSteveKingUriBuilderUri;
class RedirectController extends Controller
{
public function __invoke(Request $request, Bookmark $bookmark)
{
$url = Uri::fromString(
uri: $bookmark->url,
)->addQueryParam(
key: 'utm_campaign',
value: 'bookmarker_' . auth()->id(),
)->addQueryParam(
key: 'utm_source',
value: 'Bookmarker App'
)->addQueryParam(
key: 'utm_medium',
value: 'website',
);
return redirect(
$url->toString(),
);
}
}

Вам не нужно углубляться так сильно, как это делаю я — это часть зависит от вас. Наконец, мы можем просто добавить ссылку в пользовательский интерфейс и всё готово.

<div class="flex items-center justify-between">
<p class="text-sm font-medium text-indigo-600 truncate">
{{ $bookmark->name }}
</p>
<div class="ml-2 flex-shrink-0 flex">
<a
href="{{ route('bookmarks.redirect', $bookmark->id) }}"
target="__blank"
rel="nofollow noopener"
class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-indigo-600 bg-gray-100 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>Visit</a>
<form method="POST" action="{{ route('bookmarks.delete', $bookmark->id) }}">
@csrf
@method('DELETE')
<button type="submit" class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-red-500 bg-gray-100 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Delete
</button>
</form>
</div>
</div>

Вы можете пойти дальше, собрав статистику о том, какие закладки вы просматривали больше всего, или что-то ещё. Вы даже можете позволить кликать по тэгам, что бы посмотреть все закладки связанные с данным тэгом, но я думаю этого будет достаточно для данного руководства, чтобы понять основы.


Как вам это руководство? Написание руководства для людей незнакомых с Laravel может быть сложной задачей, и я надеюсь, мне удалось объяснить основы достаточно хорошо и подробно, что бы вы могли следовать ему.

  1. 1. Введение
  2. 2. Установка
  3. 3. Подготовка базы данных

    1. 3.1. Миграции БД
  4. 4. Модели Eloquent
  5. 5. Маршрутизация

    1. 5.1. Заглушки маршрутов
    2. 5.2. Вывод представления
  6. 6. Создание макетов и представлений

    1. 6.1. Определяем макет
    2. 6.2. Определяем дочернее представление

      1. 6.2.1. Несколько разъясняющих замечаний
  7. 7. Добавление задач

    1. 7.1. Проверка ввода

      1. 7.1.1. Переменная PHP$errors
    2. 7.2. Создание задачи
    3. 7.3. Отображение существующих задач
  8. 8. Удаление задач

    1. 8.1. Добавление кнопки удаления задачи

      1. 8.1.1. Примечание по спуфингу метода
    2. 8.2. Удаление задачи

Этот перевод актуален для англоязычной документации на

08.12.2016

(ветка

5.2) и

19.06.2016

(ветка

5.1).
Опечатка? Выдели и нажми Ctrl+Enter.

Данная статья документации актуальна только для версий 5.2 и 5.1 и была удалена в версии 5.3.

Введение

Это руководство позволит вам быстро освоить фреймворк Laravel. Оно содержит информацию о миграциях баз данных, Eloquent ORM, маршрутизации, проверке ввода, представлениях и Blade-шаблонах. Это отличная отправная точка для новичков в фреймворке Laravel и PHP-фреймворках в целом. Если вы уже использовали Laravel или другие PHP-фреймворки, вы можете ознакомиться с нашими более продвинутыми руководствами.

Чтобы рассмотреть основной набор функций Laravel, мы создадим простой список задач и будем придерживаться его (типичный пример списка «to-do»). Полный финальный вариант исходного кода для этого проекта доступен на GitHub.

Установка

Установка Laravel

Конечно, в первую очередь вам будет нужен свежий фреймворк Laravel. Чтобы запустить его, вы можете использовать виртуальную машину Homestead или локальную PHP-среду на ваш выбор. Как только ваше окружение будет готово, вы можете установить фреймворк Laravel, используя Composer:

shcomposer create-project laravel/laravel quickstart --prefer-dist

Установка проекта Quickstart (не обязательно)

Вы можете просто прочитать данное руководство. Однако, если вы хотите загрузить исходный код для этого руководства и выполнить его на локальной машине, то можете клонировать Git хранилище и установить зависимости:

shgit clone https://github.com/laravel/quickstart-basic quickstart
cd quickstart
composer install
php artisan migrate

Больше информации относительно создания локальной среды разработки Laravel вы сможете найти в документации по Homestead и по установке.

Подготовка базы данных

Миграции БД

Во-первых, давайте использовать миграцию для определения таблицы базы данных для хранения всех наших задач. Миграции БД в Laravel позволяют простым способом определить структуру таблицы базы данных и выполнять модификации с использованием простого и выразительного PHP кода. Вместо того чтобы вручную добавлять столбцы в свои локальные копии БД, ваши товарищи по команде могут просто запустить миграции, которые вы поместили в систему управления версиями.

Итак, давайте создадим таблицу БД, которая будет содержать все наши задачи. Для создания различных классов может быть использован интерфейс Artisan. Он избавит вас от ручной генерации кода при создании проектов Laravel. Поэтому давайте используем команду shmake:migration для создания миграции новой базы данных для нашей таблицы tasks:

shphp artisan make:migration create_tasks_table --create=tasks

Миграция будет помещена в каталог database/migrations вашего проекта. Как вы могли заметить, команда shmake:migration уже добавила автоинкремент ID и метки времени к файлу миграции. Давайте отредактируем этот файл и добавим дополнительный столбец string для имён наших задач:

PHP

<?phpuse IlluminateDatabaseSchemaBlueprint;
  use 
IlluminateDatabaseMigrationsMigration;

  class 

CreateTasksTable extends Migration
  
{
    
/**
    * Запуск миграций
    *
    * @return void
    */
    
public function up()
    {
      
Schema::create('tasks', function (Blueprint $table) {
        
$table->increments('id');
        
$table->string('name');
        
$table->timestamps();
      });
    }
/**
    * Откатить миграции
    *
    * @return void
    */
    
public function down()
    {
      
Schema::drop('tasks');
    }
  }

Чтобы запустить нашу миграцию, мы будем использовать команду Artisan shmigrate. Если вы используете Homestead, вы должны выполнить эту команду в своей виртуальной машине, так как у вашей host-машины не будет прямого доступа к базе данных:

shphp artisan migrate

Эта команда создаст все наши таблицы БД. Если вы просматриваете таблицы БД, используя какой-либо клиент, вы должны заметить новую таблицу tasks, которая содержит столбцы, определённые в нашей миграции. Теперь мы готовы определить модель Eloquent ORM для наших задач!

Модели Eloquent

Eloquent — это стандартное ORM для Laravel (объектно-реляционное отображение). Eloquent делает безболезненным получение и хранение данных в вашей базе данных, используя чётко определённые «модели». Обычно, каждая Eloquent модель однозначно соответствует одной таблице базы данных.

Давайте определим модель Task, которая будет соответствовать только что созданной нами таблице tasks. Мы снова можем использовать команду Artisan, чтобы сгенерировать эту модель. В этом случае мы будем использовать команду shmake:model:

shphp artisan make:model Task

Модель будет помещена в каталог app вашего приложения. По умолчанию класс модели пуст. Нам не надо явно указывать, какой таблице соответствует Eloquent модель, потому что подразумевается, что имя таблицы – это имя модели во множественном числе (s на конце). В этом случае модель Task, как предполагается, соответствует таблице базы данных tasks. Вот на что должна быть похожа наша пустая модель:

PHP

<?phpnamespace App;

  use 

IlluminateDatabaseEloquentModel;

  class 

Task extends Model
  
{
    
//
  
}

Мы ещё познакомимся чуть ближе с моделями Eloquent, когда добавим маршруты к нашему приложению. Разумеется, вы можете заглянуть и в полную документацию по Eloquent для получения дополнительной информации.

Маршрутизация

Заглушки маршрутов

Теперь можно добавить несколько маршрутов в наше приложение. Маршруты используются для связи URL с контроллерами или анонимными функциями, которые должны быть выполнены, когда пользователь переходит на данную страницу. По умолчанию все маршруты Laravel определены в файле app/Http/routes.php, который автоматически добавляется в каждый новый проект.

Для нашего приложения нам будут нужны по крайней мере три маршрута: маршрут для вывода на экран списка всех наших задач, маршрут для добавления новых задач и маршрут для удаления существующих задач. Давайте напишем заглушки для всех этих маршрутов в файле app/Http/routes.php:

PHP

<?phpuse AppTask;
  use 
IlluminateHttpRequest;/**
   * Вывести панель с задачами
   */
  
Route::get('/', function () {
    
//
  
});/**
   * Добавить новую задачу
   */
  
Route::post('/task', function (Request $request) {
    
//
  
});/**
   * Удалить задачу
   */
  
Route::delete('/task/{task}', function (Task $task) {
    
//
  
});

Если в вашей копии Laravel есть RouteServiceProvider, который уже содержит файл маршрутов по умолчанию в группе посредников web, то вам не надо вручную добавлять группу в ваш файл routes.php.

Вывод представления

Давайте заполним наш маршрут /. По этому маршруту мы хотим отрисовывать HTML-шаблон, который содержит форму добавления новой задачи, а также список всех текущих задач.

В Laravel все HTML-шаблоны хранятся в каталоге resources/views, и мы можем использовать вспомогательную функцию PHPview(), чтобы возвратить один из этих шаблонов по нашему маршруту:

PHP

Route::get('/', function () {
  return 
view('tasks');
});

Передача tasks в функцию PHPview() создаст экземпляр объекта View, который соответствует шаблону resources/views/tasks.blade.php.

Конечно, нам необходимо создать это представление, поэтому давайте сделаем это!

Создание макетов и представлений

Наше приложение содержит одно представление с формой добавления новых задач, а также список всех текущих задач. Чтобы помочь вам визуализировать представление, мы сделали скриншот законченного приложения со стандартными стилями Bootstrap CSS:

/packages/proger/habravel/uploads/374-basic-overview.png

Определяем макет

Почти все веб-приложения используют один макет на всех своих страницах. Например, у нашего приложения есть верхняя панель навигации, которая присутствовала бы на каждой странице (если бы у нас их было больше одной). Laravel упрощает использование этих общих функций на всех страницах, используя макеты Blade.

Как мы выяснили ранее, все представления Laravel хранятся в resources/views. Давайте определим представление нового макета в resources/views/layouts/app.blade.php. Расширение .blade.php даёт фреймворку команду использовать механизм шаблонной обработки Blade, чтобы отрисовать это представление. Конечно, в Laravel вы можете использовать и простые PHP-шаблоны. Однако Blade позволяет быстро написать простой и небольшой шаблон.

Наше представление app.blade.php должно выглядеть примерно так:

xml  

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Laravel Quickstart - Basic</title>

    
  </head>

  <body>
    <div class="container">
      <nav class="navbar navbar-default">
        
      </nav>
    </div>

    @yield('content')
  </body>
</html>

Обратите внимание на строчку xml@yield('content') в макете. Это специальная Blade-директива для указания всем дочерним страницам, наследующим этот шаблон, где они могут внедрить своё содержимое. Давайте определим дочернее представление, которое будет использовать этот макет и выводить его основной контент.

Определяем дочернее представление

Теперь мы должны определить представление, которое содержит форму создания новой задачи, а также таблицу со списком всех существующих задач. Давайте определим это представление в resources/views/tasks.blade.php.

Мы пропустим небольшую часть Bootstrap CSS и сфокусируемся на важном. Помните, вы можете скачать весь исходный код этого приложения с ((https://github.com/laravel/quickstart-basic GitHub):

PHP

  <!-- resources/views/tasks.blade.php -->

@extends(

'layouts.app')

@

section('content')

  <!-- 

Bootstrap шаблон... -->

  <

div class="panel-body">
    <!-- 
Отображение ошибок проверки ввода -->
    @include(
'common.errors')

    <!-- 

Форма новой задачи -->
    <
form action="{{ url('task') }}" method="POST" class="form-horizontal">
      {{ 
csrf_field() }}

      <!-- 

Имя задачи -->
      <
div class="form-group">
        <
label for="task" class="col-sm-3 control-label">Задача</label>

        <

div class="col-sm-6">
          <
input type="text" name="name" id="task-name" class="form-control">
        </
div>
      </
div>

      <!-- 

Кнопка добавления задачи -->
      <
div class="form-group">
        <
div class="col-sm-offset-3 col-sm-6">
          <
button type="submit" class="btn btn-default">
            <
class="fa fa-plus"></iДобавить задачу
          
</button>
        </
div>
      </
div>
    </
form>
  </
div>

  <!-- 

TODOТекущие задачи -->
@
endsection

Несколько разъясняющих замечаний

Прежде чем двигаться дальше, давайте немного поговорим об этом шаблоне. Во-первых, директива PHP@extends сообщает Blade, что мы используем макет, который мы определили в resources/views/layouts/app.blade.php. Все содержимое между PHP@section('content') и PHP@endsection будет добавлено вместо строчки директивы PHP@yield('content') в макете app.blade.php.

Директива PHP@include('common.errors') загрузит шаблон resources/views/common/errors.blade.php. Мы его ещё не определили, но скоро сделаем это!

Итак, мы определили основной макет и представление для нашего приложения. Помните, мы возвращаем это представление по маршруту /:

PHP

Route::get('/', function () {
  return 
view('tasks');
});

Теперь мы готовы добавить код в наш маршрут POST /task, чтобы обработать входящие данные из формы и добавить новую задачу в БД.

Добавление задач

Проверка ввода

Теперь, когда у нас есть форма на нашем представлении, мы должны добавить код к нашему маршруту POST /task в app/Http/routes.php, чтобы проверить входящие данные из формы и создать новую задачу. Во-первых, давайте проверим ввод.

Для этой формы мы создадим обязательное поле name и зададим, что оно должно содержать не более 255 символов. Если проверка не пройдёт, то мы перенаправим пользователя назад к URL /, а также возвратим ему в сессию его введённые данные с указанием на ошибки. Возврат введённых данных в сессию позволит нам сохранить их, даже если в них будут ошибки:

PHP

Route::post('/task', function (Request $request) {
  
$validator Validator::make($request->all(), [
    
'name' => 'required|max:255',
  ]);

  if (

$validator->fails()) {
    return 
redirect('/')
      ->
withInput()
      ->
withErrors($validator);
  }
// Создание задачи...
});

Переменная PHP$errors

Давайте сделаем перерыв на минутку, чтобы поговорить о строчке PHP->withErrors($validator) в нашем примере. Вызов PHP->withErrors($validator) подсветит в сессии ошибки данного экземпляра проверки ввода, и к ним можно будет обратиться через переменную PHP$errors в нашем представлении.

Помните, что мы использовали директиву PHP@include('common.errors') в нашем представлении, чтобы отобразить ошибки ввода формы. PHPcommon.errors позволяет нам легко показывать ошибки ввода в одинаковом формате на всех наших страницах. Давайте определим содержимое этого представления:

PHP

<!-- resources/views/common/errors.blade.php -->

@if (

count($errors) > 0)
  <!-- 
Список ошибок формы -->
  <
div class="alert alert-danger">
    <
strong>УпсЧто-то пошло не так!</strong>

    <

br><br>

    <

ul>
      @foreach (
$errors->all() as $error)
        <
li>{{ $error }}</li>
      @endforeach
    </
ul>
  </
div>
@endif

Переменная PHP$errors доступна в любом представлении Laravel. Если не будет ошибок ввода, она просто будет пустым экземпляром PHPViewErrorBag.

Создание задачи

Теперь, когда обрабатывается ввод данных, давайте создадим новую задачу, продолжая заполнять наш маршрут. Как только новая задача была создана, мы перенаправим пользователя назад к URL /. Чтобы создать задачу, мы можем использовать метод PHPsave() после создания и установки свойств для новой модели Eloquent:

PHP

Route::post('/task', function (Request $request) {
  
$validator Validator::make($request->all(), [
    
'name' => 'required|max:255',
  ]);

  if (

$validator->fails()) {
    return 
redirect('/')
      ->
withInput()
      ->
withErrors($validator);
  }
$task = new Task;
  
$task->name $request->name;
  
$task->save();

  return 

redirect('/');
});

Отлично! Теперь мы можем создавать задачи. Давайте продолжим создание нашего представления, добавив список всех существующих задач.

Отображение существующих задач

Во-первых, мы должны отредактировать наш маршрут /, чтобы передать все существующие задачи в представление. Функция PHPview() принимает массив данных вторым параметром, который будет доступным для представления. Каждый ключ массива станет переменной в представлении:

PHP

Route::get('/', function () {
  
$tasks Task::orderBy('created_at''asc')->get();

  return 

view('tasks', [
    
'tasks' => $tasks
  
]);
});

Когда данные переданы, мы можем обращаться к задачам в нашем представлении tasks.blade.php и выводить их на экран таблицей. Blade-конструкция PHP@foreach позволяет нам кратко писать циклы, которые компилируются в молниеносный простой PHP-код:

PHP

@extends('layouts.app')

@

section('content')
  <!-- 
Форма создания задачи... -->

  <!-- 

Текущие задачи -->
  @if (
count($tasks) > 0)
    <
div class="panel panel-default">
      <
div class="panel-heading">
        
Текущая задача
      
</div>

      <

div class="panel-body">
        <
table class="table table-striped task-table">

          <!-- 

Заголовок таблицы -->
          <
thead>
            <
th>Task</th>
            <
th>&nbsp;</th>
          </
thead>

          <!-- 

Тело таблицы -->
          <
tbody>
            @foreach (
$tasks as $task)
              <
tr>
                <!-- 
Имя задачи -->
                <
td class="table-text">
                  <
div>{{ $task->name }}</div>
                </
td>

                <

td>
                  <!-- 
TODOКнопка Удалить -->
                </
td>
              </
tr>
            @endforeach
          </
tbody>
        </
table>
      </
div>
    </
div>
   @endif
@
endsection

Наше приложение почти готово. Но у нас нет способа удалять наши существующие задачи, когда они завершены. Давайте добавим и это!

Удаление задач

Добавление кнопки удаления задачи

Мы оставили отметку «TODO» в коде, где предположительно будет находиться наша кнопка. Давайте добавим кнопку удаления к каждой строке нашего списка задач в представлении tasks.blade.php. Мы создадим маленькую однокнопочную форму для каждой задачи в списке. После нажатия кнопки приложению будет отправляться запрос DELETE /task:

PHP

<tr>
  <!-- 
Имя задачи -->
   <
td class="table-text">
    <
div>{{ $task->name }}</div>
  </
td>

  <!-- 

Кнопка Удалить -->
  <
td>
    <
form action="{{ url('task/'.$task->id) }}" method="POST">
      {{ 
csrf_field() }}
      {{ 
method_field('DELETE') }}

      <

button type="submit" class="btn btn-danger">
        <
class="fa fa-trash"></iУдалить
      
</button>
    </
form>
  </
td>
</
tr>

Примечание по спуфингу метода

Обратите внимание на то, что method формы кнопки удаления объявлен как POST, несмотря на то, что мы отвечаем на запрос, используя маршрут PHPRoute::delete. HTML-формы позволяют использовать только GET и POST методы HTTP. А нам нужен способ имитировать запрос DELETE от формы.

Мы можем имитировать запрос DELETE, выводя результаты функции PHPmethod_field('DELETE') в нашей форме. Эта функция генерирует скрытый ввод формы, который распознается Laravel и используется, чтобы переопределить вызываемый метод HTTP. Сгенерированное поле будет похоже на это:

PHP

<input type="hidden" name="_method" value="DELETE">

Удаление задачи

Наконец, давайте добавим к нашему маршруту логику удаления текущей задачи. Мы можем использовать неявную привязку модели, чтобы автоматически получить модель Task, которая соответствует параметру маршрута {task}.

В обратном вызове нашего маршрута мы используем метод PHPdelete() для удаления записи. Как только запись удалена, мы перенаправим пользователя назад к URL /:

PHP

Route::delete('/task/{task}', function (Task $task) {
  
$task->delete();

  return 

redirect('/');
});

Документация Laravel 9.x

Вольный перевод репозитория документации laravel/docs ветка 9.x на русский язык. Актуализация с основным репозиторием документации Laravel осуществляется не реже одного раза в месяц. Орфографические, пунктуационные и грубые смысловые ошибки исправляются по мере их выявления.

Перевод документации Laravel ветка 8.x.

Содержание документации

Перед чтением документации ознакомьтесь с соглашением по переводу.

Разделы, помеченные галочками, уже имеют актуализированные переводы полностью на русском языке.

  • Пролог

    • Примечания к релизу
    • Руководство по обновлению
    • Рекомендации по участию
  • Начало

    • Установка
    • Конфигурирование
    • Структура каталогов
    • Внешний интерфейс приложения
    • Стартовые комплекты
    • Развертывание
  • Архитектурные концепции

    • Жизненный цикл запроса
    • Контейнер служб
    • Поставщики служб
    • Фасады
  • Основы

    • Маршрутизация
    • Посредники
    • Предотвращение атак CSRF
    • Контроллеры
    • HTTP-запросы
    • HTTP-ответы
    • HTML-шаблоны
    • Шаблонизатор Blade
    • Объединение веб-активов
    • Генерация URL-адресов
    • Сессия HTTP
    • Валидация
    • Обработка ошибок
    • Логирование
  • Продвинутое руководство

    • Консоль Artisan
    • Трансляция событий
    • Кеш приложения
    • Коллекции
    • Контракты
    • События
    • Файловое хранилище
    • Глобальные помощники
    • HTTP-клиент
    • Локализация интерфейса
    • Почтовые отправления
    • Уведомления
    • Разработка пакетов
    • Очереди
    • Ограничители частоты
    • Планирование задач
  • Безопасность

    • Аутентификация
    • Авторизация
    • Подтверждение адреса электронной почты
    • Шифрование
    • Хеширование
    • Сброс пароля
  • База данных

    • Начало работы
    • Построитель запросов
    • Постраничная навигация
    • Миграции
    • Наполнение фиктивными данными
    • Использование Redis
  • Eloquent ORM

    • Начало работы
    • Отношения
    • Коллекции
    • Мутаторы и типизация
    • Ресурсы API
    • Сериализация
    • Фабрики
  • Тестирование

    • Начало работы
    • Тесты HTTP
    • Тесты консольных команд
    • Браузерные тесты
    • База данных
    • Имитация
  • Пакеты

    • Breeze – легковесная реализация аутентификации Laravel для ознакомления с функционалом. Включает простые шаблоны Blade, стилизованные с помощью Tailwind CSS. Содержит маршруты для публикации.
    • Dusk – автоматизация поведения браузера и тестирование с использованием ChromeDriver.
    • Envoy – инструмент для запуска задач, выполняемых на удаленных серверах. Задачи определяются в файле Envoy.blade.php в корне приложения с использованием директив шаблонизатора Blade.
    • Fortify – серверная реализация аутентификации Laravel. Не содержит никаких шаблонов. Используется в Laravel Jetstream.
    • Homestead – официальный образ Vagrant для приложений Laravel.
    • Horizon – панель управления и конфигурация очередей, использующих Redis.
    • Jetstream – красиво оформленный каркас приложений. Включает в себя Fortify и Sanctum.
    • Mix – гибкий API для определения шагов сборки Webpack; упрощает компиляцию и минимизацию файлов CSS и JavaScript.
    • Octane – повышает производительность вашего приложения с использованием мощных серверов Swoole и RoadRunner
    • Passport – реализация сервера OAuth2 для вашего приложения Laravel на основе League OAuth2.
    • Pint – is an opinionated PHP code style fixer for minimalists.
    • Sail – CLI для взаимодействия со средой разработки Docker.
    • Sanctum – легковесная система аутентификации для SPA (одностраничных приложений), мобильных приложений и простых API на основе токенов. Управление токенами API, аутентификация сессии. Не содержит никаких шаблонов. Используется в Laravel Jetstream.
    • Scout – «простое» решение на основе драйверов для добавления полнотекстового поиска моделям Eloquent.
    • Socialite – аутентификация через провайдеров OAuth: Facebook, Twitter, LinkedIn, Google, GitHub, GitLab и Bitbucket.
    • Telescope – панель управления, отображающая записи о произошедших в приложении событиях.
    • Valet – окружение разработки приложений Laravel для пользователей macOS.
  • Документация API

Содействие переводу

Официальная документация Laravel доступна только на английском языке. Для получения дополнительной информации о том, как вы можете внести свой вклад в перевод документации Laravel на русский язык, пожалуйста, прочтите Содействие в переводе документации.

Лицензия

Ссылка на лицензию оригинала документации laravel/docs.

Текущий перевод документации распространяется по лицензии MIT.

Итак, у вас есть желание попробовать или узнать о фреймворке Laravel.

Если вы хорошо знакомы с другими PHP фреймворками — для вас это не составит особого труда, если же нет — это отличный выбор для первого фреймворка.

Laravel - PHP framework for artisans!

Статья очень большая. Рекомендую читать ее полностью во время выходных.

Для ленивых:
GitHub
Приложение

Установка

Для установки Laravel нам потребуется Composer

Composer является инструментом для управления зависимостями в PHP. Он позволяет объявлять зависимые библиотеки, необходимые для проекта, и устанавливать их в проект.
— Composer

Установка окружения будет происходить в среде *nix (на сайте так же есть мануал по установке на Windows, плюс к этому вам нужен будет сервер, например WAMP и Git).

Предположим, что у Вас совсем чистенькая ОС. Тогда откройте терминал и

введите эти строчки

скопируйте и вставьте

# Установка недостающих компонентов
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

Через некоторое время у вас будут установлены все необходимые инструменты.
Перейдем непосредственно к установке Laravel.

# Предпочитаемая мной структура папок
cd # перейдем в директорию /home/%user%
mkdir workspace #создадим папку workspace
cd workspace # перейдем в нее
mkdir php # создадим папку php
cd php # перейдем в папку php

Создадим проект laravel в папке habr

composer create-project laravel/laravel habr --prefer-dist 
# .... тут будет долгий процес создания проекта ....

Перейдем в созданный проект и убедимся, что все работает, запустив команду php artisan serve

cd habr
php artisan serve

Локальный сервер будет доступен по адресу http://localhost:8000.

На всякий случай artisan — это скрипт для командной строки, который есть в Laravel. Он предоставляет ряд полезных команд для использования при разработке. Он работает поверх компонента консоли Symfony. (Artisan CLI). Есть много полезных команд, с помощью которых в командной строке можно создавать разные полезные вещи. Для списка команд введите php artisan list в командной сроке.

Перейдя по адресу http://localhost:8000 вы должны увидеть красивую заставку как в начале поста.

Настройка

Для соединения с базой данных (далее БД) у Laravel есть конфигурационный файл database.php, находится он в папке app/config/.
Сначала создадим БД и пользователя в 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

Отлично! У нас есть все данные для доступа к MySQL: пользователь habr с паролем my_password и БД habr на хосте localhost. Перейдем в файл конфигурации БД и изменим наши настройки.

Laravel файл конфигурации БД

В Laravel есть отличные инструменты — Миграции и Построитель Схем.

Миграции это тип управления версиями в базе данных. Они позволяют команде разработчиков изменять схему базы данных и оставаться в курсе о текущем состоянии схемы. Миграция, как правило, в паре с Построителем Схем позволют легко управлять схемой БД.
— Миграции
Построитель Схем — это класс Schema. Он дает возможность манипулирования таблицами в БД. Он хорошо работает со всеми БД, которые поддерживаются Laravel, и имеет единый API для всех этих систем.
— Построитель Схем

Во первых создадим таблицу миграций:

php artisan migrate:install

Если настройки соединения с БД правильны, то мы готовы создавать миграции и таблицы.
Но перед этим хочу вас познакомить с установкой дополнительных пакетов, которые можно использовать для более эффективного и быстрого создания веб приложения.

Laravel 4 Generators

Мега полезный инструмент — generators от Jeffrey Way. GitHub.

Он добавляет в список artisan много полезных команд, таких как:

  • generate:model — создание моделей
  • generate:controller — создание контроллеров
  • generate:seed — создание файлов для наболнения БД конфигурационной / фейковой информацией
  • generate:view — создание шаблонов
  • generate:migration — создание миграций
  • generate:resource — создание ресурсов
  • generate:scaffold — создание прототипов (самое интересное, его рассмотрим подробнее чуть позже!)
  • generate:form — создание форм
  • generate:test — создание тестов
  • generate:pivot — создание миграции сводной таблицы
Установка пакета

Установка пакетов с помощью Composer происходит достаточно просто. Нужно отредактировать файл composer.json в корне приложения, добавив строчку "way/generators": "1.*" в список "require".

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

После этого нужно обновить зависимости проекта. Введите в терминале

composer update

Последним штрихом будет занесение в кофигурационный файл app/config/app.php в список провайдеров приложения строки

'WayGeneratorsGeneratorsServiceProvider'

Теперь список команд php artisan будет также содержать новые команды generate. В следующем разделе я покажу как использовать generate для создания приложения и ускорения разработки.

Создание приложения

Предположим, что мы создаем некий

блог

сайт со скидками. Для этого нам нужно:

  • Таблица пользователей с имейлом, username и паролем
  • Таблица ролей
  • Таблица ролей пользователей
  • Таблица городов
  • Таблица компаний
  • Таблица тегов
  • Таблица скидок с полями: заголовок, описание, город, компания, % скидки, картинка и дата истечения скидки
  • Таблица комментариев с оценками
  • Таблица тегов скидок

Набросаем схему таблиц в БД. У меня получилось что-то такое:
Initial DB Schema

За это спасибо generator‘у. Так как все, что я сделал — это прописал 10 строк, кстати, вот и они:

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 

С помощью последней команды в БД будут занесены все миграции, которые еще не были записаны. Важно то, что все новые миграции будут запущены одним стэком. Для того, чтобы откатить миграцию есть команда php artisan migrate:rollback, а для того, чтобы откатить все миграции до нуля migrate:reset, чтобы скатить до нуля и запустить все миграции migrate:refresh.

В Laravel версии выше 4.1.25 произошло обновление безопасности, где закрывали дыру с похищенными куками. Подробности обновления и инструкцию можно посмотреть тут: http://laravel.com/docs/upgrade для тех, у кого версия Laravel < 4.1.26. Или просто прочтите коммент от vlom88 http://habrahabr.ru/post/197454/#comment_7510479.

Подробнее о командах генератора:

  • generate:migration Принимает имя аргумент миграции, и создает соответсвующую схему. В имени схемы можно указать ключевые слова, например create — создание, далее идет имя таблицы и ключевое слово table. Так же можно указать какие поля добавить в таблицу через опцию —fields=»», в которой через запятую перечислить поля с ихним типом данных. Создание миграции, Типы данных и прочее
  • generate:scaffold Принимает как агрумент ресурс (к примеру role), и создает такие файлы:
    • app/models/Role.php — клас модели, наследуемый от Eloquent ORM для работы с таблицей ролей (имя самой таблицы — это множественное число от имени ресурса)
    • app/controllers/RolesController.php — клас контроллера, который отвечает на запросы к сайту, так же является REST контроллером
      Метод HTTP Путь (URL) Действие Имя маршрута
      GET /resource index resource.index
      GET /resource/create create resource.create
      POST /resource store resource.store
      GET /resource/{id} show resource.show
      GET /resource/{id}/edit edit resource.edit
      PUT/PATCH /resource/{id} update resource.update
      DELETE /resource/{id} destroy resource.destroy

    • app/views/roles/index.blade.php — шаблон, который отвечает за список всех ресурсов (обычно генерируется при GET запросе по URL /roles), про сам шаблонизатор я расскажу чуть позже
    • app/views/roles/show.blade.php — шаблон, который отвечает за отображение конкретного ресурса (GET запрос на URL /roles/{id})
    • app/views/roles/create.blade.php — шаблон, в котором находится форма для добавления ресурса (GET на URL /roles/create)
    • app/views/roles/edit.blade.php — шаблон, в котором находится форма для редактирования ресурса (GET на URL /roles/{id}/edit})
    • app/views/layouts/scaffold.blade.php — основной лейаут приложения (содержит базовый html + bootstrap + контейнер для вставляемого контента)
    • app/database/migrations/Create_roles_table.php — миграция
    • app/database/seeds/RolesTableSeeder.php — файл для тестового наполнения таблицы данными
    • app/tests/controllers/RolesTest.php — различные тесты

    а так же обновляет и добавляет данные в файлы

    • app/database/seeds/DatabaseSeeder.php — добавляет вызов RolesTableSeeder
    • app/routes.php — добавляет в регистр маршрутов все методы ресурса (REST)

  • generate:pivot Принимает 2 аргумента (имена таблиц). Создает сводную таблицу, которая содержит 2 foreign key

Я надеюсь этот пример использования генератора достаточно наглядно показал, каким образом его использовать и насколько он полезен.

Чего нам еще не хватает — так это некоторых связок между таблицами.

Важно знать! При добавлении foreign key к колонке в таблице нужно убедится, что колонка является unsigned.

Что ж, добавим их:

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

Теперь нам нужно прописать добавление индексов внутри самих файлов миграций, так как такие изменения автоматически не создаются.

...
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');
		});
	}
}

Взгянув на схему БД видим ситуацию по лучше
Cool DB Schema

На данный момент все ссылки на ресурсы являются открытыми, и по ним можно переходить всем кому угодно.
Допустим, добавим роль admin. По ссылке http://localhost:8000/roles видим следующую картину:
Admin role added

Немного о шаблонах и шаблонизаторе Blade в Laravel.
Для файлов шаблонов используется раширение

.blade.php

. Заглянув в файл app/views/layouts/scaffold.blade.php мы видим

// 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>

Что здесь происходит? Сам файл является скелетом, лэйаутом, который можно расширить, добавив внутрь секции main какой-то контент, или еще один шаблон. Двойные фигурные скобки {{ $var }} являются аналогом <?php echo $var; ?>. Класс Session используется здесь для вывода сообщений пользователю, если мы передадим какое-то сообщение. Сообщение является временным, и при обновлении страницы пропадет. Если мы откроем только что созданный шаблон 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

То нам станет ясно, что этот шаблон расширяет шаблон app/views/layouts/scaffold.blade.php, за это говорит код @extends('layouts.scaffold'). Заметьте, что тут для разделения между папками используется точка, хотя так же можно использовать и /.

Далее в секцию main будет записано все до первого появления @stop. Так же тут используются знакомые нам if - else - endif и foreach - endforeach, вспомогательная функция link_to_route, которую нам предоставляет Laravel (Helper Functions) и класс Form для создания форм (Предпочтительно нужно пользоваться им, хотя бы Form::open(), так как он создает дополнительный аттрибут формы _token — защита от подделки кросс сайтовых запросов и _method в случае PUT / PATCH или DELETE).

Первым делом подумаем о защите всех ресурсов. Для этого нам нужно ввести авторизацию.

Создадим новый контроллер LoginContoller в папке app/controllers

php artisan generate:controller LoginController

И добавим для него несколько шаблонов

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"

Теперь изменим сам контроллер. Нам нужны 5 методов:

  • index — отвечает за генерацию формы входа
  • register — отвечает за генерацию форми регистрации
  • store — отвечает за регистрацию нового пользователя
  • login — отвечает за вход пользователя на сайт
  • logout — отвечает за выход пользователя

Измененный контроллер LoginController будет выглядеть так:

// 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!');
	}

}

Первые два метода генерируют из шаблонов HTML.
Метод store сохраняет в нашу БД нового пользователя, принимая все входящие через POST данные от Input::all(). (Подробнее).
В классе Input находятся данные, которые были отправлены при POST запросе. Он имеет ряд статичных методов, таких как all(), get(), has() и другие (Basic Input).

Hash — это класс шифрования, который использует метод bcrypt, чтобы пароли в БД хранились в зашифрованом виде (Laravel Security).

Но перед регистрацией нам нужно провести валидацию входящих данных.
Для этого в Laravel есть класс Validator. Метод Validation::make принимает 2 или 3 аргумента:

  1. $input — обязательный, массив входящих данных, которые нужно проверить
  2. $rules — обязательный, массив с правилами к входящим данным
  3. $messages — опциональный, массив с сообщениями об ошибках

Полный список доступных правил можно посмотреть тут Available Validation Rules.

Метод fails() возвращает true или false в зависимости от того, прошли ли валидацию данные в соответствии с правилами, которые мы передали в метод make.

Класс Redirect используется для перенаправления. Его методы:

  • back() — перенаправит на страницу, с которой был послан запрос
  • intended(‘fallback’) — перенаправит на страницу, с которой пользователь попал под фильтр авторизации, если таковой не было, то отправит на URL, который передан в fallback
  • withInput() — передаст во временную сессию данные с Input
  • withErrors($validator) — передаст в переменную $errors данные с $validator (! Важно знать, что переменная $errors создается на всех страницах при GET запросах, поэтому она всегда доступна на всех страницах).
  • with(‘variable’, ‘Your message here’) — передаст во временную сессию переменную ‘variable’ с сообщением, которое вы укажете

Класс Auth является классом авторизации, у него имется ряд методов, в том числе и loginUsingId($id), который авторизирует пользователя по указанному идентификатору из БД (Authenticating Users). Так как после регисрации мы хотим автоматически авторизировать пользователя, то воспользуемся им.

Метод нашего Контроллера login() авторизирует пользователя по email или username и перенаправляет на страницу, с которой он попал под фильтр авторизации. В случае не совпадения данных, перенаправляет обратно с входящими данными, сообщением о ошибке, но без пароля.

Таким образом у нас есть Контроллер, который отвечает за авторизацию.

Следующим шагом для скрытия всех ресурсов от доступа будет изменение файла app/routes.php, который содержит маршруты приложения.

// 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();
	}
});

Перейдя теперь по ссылке, к примеру /roles нас будет перенаправлено на страницу /login, на которой пока отображается только стандартный текст "index.blade.php".

Ко всем маршрутам, заключенным в Route::group(array('before' => 'admin.auth')) будет применятся фильтр admin.auth, который проверяет, является ли пользователь гостем, или нет, и в случае, если является — отправит его на страницу входа. Про фильтры можно почитать тут, а про группировку маршрутов тут. Другой фильтр Route::group(array('before' => 'un_auth')) будет проверять, является ли пользователь вошедшим на сайт, и если проверка выполнятся — то он его разлогинивает.

Для нормальной работы изменим файлы логина и регистрации:

// 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

Как вы заметили, тут я использовал новый прием в шаблонизаторе @include('view', $variable). В применении он весьма прост — передайте 2 аргумента:

  1. view — шаблон, который нужно включить в конкретный шаблон
  2. $variable — переменная, которую нужно передать для отрисовки шаблона

Зарегистрируйтесь на сайте, чтобы иметь доступ к сайту.

Что же, теперь можна заняться ресурсами. Начнем с городов. Первым делом изменим в Модели City правила валидации:

// 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'
	);
}

После нее изменим правила валидации так же и у Моделей Company, Role и 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'
	);
	...

Для удобства перехода между ссылками добавим меню в app/views/layouts/scaffold.blade.php, а так же добавим jQuery и jQuery-UI для будующих нужд

// 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>

Далее перейдем к редактированию правил валидации в Модели 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'
	);

Здесь я использовал сложный паттерн для поля image, так как хочу воспользоваться средствами AJAX для загрузки картинок, и в саму валидацию передавать только путь к картинке на сервере. Значит начнем с изменения шаблона app/views/offers/create.blade.php и создания отдельного файла для скриптов.

// 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>

Здесь мы будем добавлять картинку по нажатию на input[name="file"] и отправлять ее с помощью AJAX по URL /upload. Ответом с этого URL будет ссылка на загруженное изображение. Эту ссылку мы вставим в атрибут src у картинки #thumb и сохраним в скрытом инпуте image. Дальше нам нужно в файле 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'));
}
...

Отлично, URL мы зарегистрировали, осталось прописать логику в HomeController. Для этого в файле app/controllers/HomeController.php добавим метод uploadOfferImage
min:

// 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));
	}
}

Все достаточно просто: правила, валидация, ошибки, ответ. Что бы сохранить для начала мы зададим папку, в которую будем его сохранять — это public_path()/images/текущий год/месяц/дата/ (public_path() — это вспомогательная функция Laravel для пути к публичным файлам), далее создадим рандомное имя файла str_random(30) длиною 30 символов и расширением jpg. После этого воспользуемся классом Input и его методом file('file')->move('destination_path', 'filename'), где: ‘file’ — входящий файл, ‘destination_path’ — папка, в которую перемещаем файл, ‘filename’ — имя для файла, который будет сохранен.
Response::json выдаст ответ в формате json.
Отлично! Файлы у нас теперь загружаются с помощью AJAX.
AJAX upload Laravel
Следующим шагом будет изменение Form::input('number', 'city_id') и Form::input('number', 'company_id') на селекты с реальными данными.

// 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>
	...

Как работают селекты можно глянуть тут Forms & Html (Dropdown Lists). Таким образом мы имеем возможность выбирать из существующих городов и компаний в БД.

Чего нам еще не хватает — так это добавление тегов к скидкам. Тут нам поможет jquery-ui с autocomplete для добавления нескольких значений. Для этого расширим файл с скриптами 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>

Это стандартный пример использования с сайта jqueryui.com, только немного модифицированный в точке ответа с сервера. Как вы видите, обращение идет по адресу /tags. Организуем логику ответа на AJAX запрос по этому 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'));
	}
	...

Интересно то, что Eloquent преобразуется в формат json, если мы ее возвращаем, поэтому здесь нет необходимости использовать Response::json(). И вот у нас автодополняются теги.

Последнее, что нам нужно сделать — это изменить логику создания скидок.

// 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('Y-m-d', strtotime('+1 day')).'|before:'.date('Y-m-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.');
	}
	...

Во первых, расширим правило expires, что бы скидка заканчивалась не раньше завтрашнего дня, и не позже, чем через 1 месяц. Далее выделим все id тегов в отдельный массив, проверив их наличие в БД. После идет небольшая проверка, введены ли теги. А под самый конец очень интересный прием: в Eloquent для связки таблиц можна использовать разные отношения (Eloquent Relationships), к примеру, у Модели Offers может быть много тегов, соответсвенно пропишем это в Модели

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

Таким образом мы создали связь между одной записью в таблице offers и многими записями в таблице tags. Теперь, обращаясь к методу $offer->tags() мы можем получить все теги, к которым привязана конкретная скидка. Но в данном примере у нас еще используется специальный метод для работы с промежуточными таблицами sync(array(1, 2, 3)), который запишет в промежуточную таблицу к offer_id нужные tag_id. Таблица offer_tag:
Pivot table offer to tag
Также нам нужно указать связь между записью в таблице offers и записями в таблицах cities и companies:

// 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("rn", '<br>', e($str));
		return $str;
	}
}

Осталось изменить файл 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

И мы видим отличную картину, которая полностью отображает структуру скидки:
All offers

{{{ $string }}} выводит содержимое $string, предварительно прогнав через htmlentities, то бишь конвертирует не безопасные символы, что защищает от XSS. Аналогом является <?php echo htmlentities($string); ?> или вспомогательной функции Laravel e($string)

Теперь осталось изменить app/views/offers/edit.blade.php, app/views/offers/show.blade.php и метод update в app/controllers/OfferController.php.

Код для 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

Изменения очень схожы с app/views/offers/create.blade.php, только есть небольшая разница в и {{ Form::text('tags', ... }}. С картинкой все понятно: если есть старый инпут — заменяем на него, если его нет — то на значение image нашей скидки. В Form::text('tags', ... ) мы, во первых, взяли все теги, которые относятся к конкретной скидке $offer->tags() и выняли из БД только поля name. Далее воспользовались вспомогательной функцией от Laravel array_fetch, что бы у нас получился одномерный массив, а в конце соединили этот массив в строку, вставив запятую и пробел между ними.

Изменим метод update в 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('Y-m-d', strtotime('+1 day')).'|before:'.date('Y-m-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.');
	}
	...

Различие с методом добавления минимальны. Во первых, выбросим 404 ошибку, если задан неправильный id, во вторых будем использовать метод update($id). Вот и все изменения.

Далее изменим файл 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>
		...

Теперь и после изменения скидки у нас будет красиво выводится ее структура с изображением и всеми реляционными данными.

Главная страница сайта

Настало время наконец то для создания главной страницы сайта.

Для начала создадим новый 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>

А так же файл стилей:

// 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;}

Потом переопределим маршрут главной страницы:

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

Добавим в HomeController недостающий метод 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'));
	}
	...

Создадим папку app/views/homeи добавим туда файл index.blade.php, а так же создадим файл _preview.blade.php в папке app/views/offers

// 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>

Далее нужно добавить поиск скидок по тегам, городам и компаниям. Для этого добавим 3 маршрута в файл app/routes.php сразу же за 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 -_]+');
...

Теперь добавим недостающие методы в 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'));
	}
	...

Для корректной работы этих методов нам нужно задать связи в Моделях City, Company и 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');
	}

Что бы все это дело заиграло, изменим файл app/views/offers/_preview.blade.php, добавив ссылок:

// 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>

Кликаем, переходим, скидки сортируются и выводятся в соответствии с критериями.

Теперь сделаем представление для просмотра отдельной скидки:

// 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;}

Для того, чтобы можно было просматривать скидку полностью, добавим маршрут и метод, а так же в конце я добавил форму для комментариев. Для ее работоспособности также нужно добавить маршрут и метод в нужном контроллере:

// 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);
	}
	...

Разберемся со всем по порядку:

  • С представлением скидки, надеюсь, проблем нет — это все та же верстка + шаблонизатор.
  • В маршрутах тоже все просто, все по аналогии как и раньше: ссылка — контроллер@метод, разве что Route::post('/offer_{id}'...) использует новый фильтр, который без авторизации выдает кастомное сообщение.
  • showOffer($id) тоже ничего сложного из себя не представляет.
  • Интересен сам метод добавления комментариев. Во первых, проверим, правильный ли id нам передали.

    Далее идет работа с промежуточной таблицей offers для скидки и пользователя. Эту связь нужно указать в Модели Offer

    // app/models/Offer.php
    	...
    	public function usersComments()
    	{
    		return $this->belongsToMany('User', 'comments')->withPivot('body', 'mark')->withTimestamps();
    	}
    	...
    

    Как видите, мы тут явно задаем таблицу comments как промежуточную, и указываем, что так же в этой таблице содержатся дополнительные колонки body и mark + в этой таблице используются штампы времени (создания и обновления).

    Используя проверку, есть ли уже комментарий к конкретной скидке от текущего пользователя (метод contains()), перенаправляем обратно. Если же нет — то прикрепляем новый комментарий от пользователя к скидке с его оценкой и текстом.

Для вывода комментариев на странице скидки изменим немного файл app/views/offers/_show.blade.php

// 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("rn", '<br>', e($user->pivot->body)) }}</p>
	</div>
</div>
@endforeach
@stop

Теперь под скидками пользователи могут оставлять свои комментарии, каждый по одному, и если пользователь уже оставил комментарий — то форма не будет для него выводиться.

Следующим шагом будет распределить права доступа к сайту. Для начала укажем связь между пользователями и ролями:

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

Далее добавим в админке управление ролями пользователей:

// 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>
...

Помним, что в Модель User нужно добавить связь с ролями:

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

Создадим контроллер 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');
	}

}

Создадим папку app/views/users и добавим туда 3 файла:

// 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

А так же изменим немного метд index контроллера RolesController

	...
	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'));
	}
	...

Теперь автодополнение работает.

Далее, для того, что бы у нас с вами не было разбежностей, откатим все миграции и воспользуемся отличным инструментом, который нам предоставляет Laravel — это DatabaseSeeder. С помощью него мы можем наполнить нашу БД какими-то конфигурационными, или стартовыми / тестовыми данными. Для этого сначала создадим класс UsersTableSeeder в папке app/database/seeds:

// 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);
	}

}

Логика такова: очищаем таблицу, создаем массив данных и вставляем в БД.

Проделаем то же самое с 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);
	}

}

Здесь я так же добавил роли manager и moderator, что бы давать пользователям с этими ролями доступ к отдельным ресурсам в админ панели.

Далее создадим еще один класс 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);
	}

}

Таким образом мы добавили роль admin нашему первому пользователю.

Чтобы очистить БД и заполнить ее нашими начальными данными сначала изменим файл app/database/seeds/DatabaseSeeder.php таким образом:

// 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');
	}

}

И для принятия всех изменений запустим через консоль команду (находясь в папке /workspace/php/habr/):

php artisan migrate:refresh --seed

migrate:refresh откатит все миграции, а потом их снова запустит, а опция --seed укажет на то, что так же нужно запустить DatabaseSeeder.

Далее выстроим логику на права. Внесем изменения в Модель 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);
	}
}

Далее изменим файл маршрутов, что бы он соответствовал правам пользования сайтом:

// 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.');
		}
	}
});

Как вы заметили, я добавил к маршруту комментирования дополнительный фильт. Таки образом, никто, кроме обычных пользователей сайта не сможет оставлять комментарии к скидкам.

Также тут был использован маршрут Route::when() — это так называемый шаблонный фильтр (Pattern Filter). Он позволяет первым параметром передать шаблон URL, вторым — сам фильтр, который нужно применить, а третьим параметром он может принимать массив из HTTP запросов, к которым нужно применить фильтр.

Изменим метод login() контроллера 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!');
	}

Теперь при входе на сайт обычные пользователи будут попадать на главную страницу, а администраторы, модераторы и менеджеры в админпанель.

Изменим немного навигационное меню для администрации:

// 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

Отлично — теперь каждой роли будут видны те ресурсы, к которым у них есть доступ.

Emails

Важным аспектом для web приложения является отправка почты.

Laravel использует SwiftMailer для создания писем (Laravel Mail).

Для начала нужно сконфигурировать настройки отправки почты. В качестве демонстрации для отправки писем я буду использовать свой аккаунт на gmail, но вы можете пользоваться по сути любым сервисом, который предоставляет возможность отправки почты с его серверов (к примеру Postmarkapp).

Настройка почты:

// 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
);

Параметр pretend отвечает за то, нужно ли отправлять письма. Если его выставить в true, то оправка писем происходить не будет, но в логах сайта (app/storage/logs) будут сохраняться отчеты об отправке.

Первым делом я хочу, чтобы при регистрации пользователю отправлялось письмо с приветствием, для этого создам шаблон в папке app/views/emails:

// 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>

Далее изменим метод store() нашего 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);
...

Класс Mail для отправки почты использует метод send(), который принимает три аргумента:

  • $view — шаблон, который нужно использовать (или массив из двух шаблонов, первый — html шаблон, второй — plaintext)
  • $data — массив данных, ключи которого будут переменными в шаблоне
  • $callback — функцию, которая будет запущена для настройки параметров письма

Но приветственное письмо — это не единственный тип писем, который нам нужен. Что если пользователь забыл свой пароль и хочет его восстановить? Для этого Laravel предоставляет Password Reminders & Reset.
Что нам нужно сделать:

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

Для восстановления пароля достаточно вызова Password::remind(array('email' => $email)) и письмо с ссылкой на восстановление пароля будет отправлено.

Нам потребуется создать 2 шаблона:

  • app/views/auth/remind.blade.php — для отправки email на восстановление пароля
    // app/views/auth/remind.blade.php
    @extends('layouts.scaffold')
    
    @section('main')
    
    @if (Session::has('error'))
    	<div class="alert alert-error">
    		{{ trans(Session::get('reason')) }}
    	</div>
    @elseif (Session::has('success'))
    	<div class="alert alert-success">
    		An e-mail with the password reset has been sent.
    	</div>
    @endif
    
    <h1>Forgot your password?</h1>
    
    <p>{{ link_to_route('login.index', 'No') }}</p>
    
    {{ Form::open() }}
    	<ul>
    		<li>
    			{{ Form::label('email', 'Your email')}}
    			{{ Form::email('email') }}
    		</li>
    
    		<li>
    		{{ Form::submit('Send reminder', array('class' => 'btn')) }}
    		</li>
    	</ul>
    {{ Form::close() }}
    
    @stop
    

  • app/views/auth/reset.blade.php — форма восстановления пароля
    // app/views/auth/reset.blade.php
    @extends('layouts.scaffold')
    
    @section('main')
    
    @if (Session::has('error'))
    	<div class="alert alert-error">
        	{{ trans(Session::get('reason')) }}
    	</div>
    @endif
    
    <h1>Reset your password</h1>
    
    {{ Form::open() }}
    {{ Form::hidden('token', $token) }}
    	<ul>
    		<li>
    			{{ Form::label('email', 'Email')}}
    			{{ Form::email('email', Input::old('email')) }}
    		</li>
    
    		<li>
    			{{Form::label('password', 'New password')}}
    			{{ Form::password('password')}}
    		</li>
    
    		<li>
    			{{Form::label('password', 'New password confirmation')}}
    			{{ Form::password('password_confirmation')}}
    		</li>
    
    	</ul>
    {{ Form::submit('Reset', array('class' => 'btn'))}}
    {{ Form::close() }}
    @stop
    

Функция trans() — вспомогательная функция, которая выводит локализированную строку из конфигурации. Можете заглянуть в папку app/lang/en/reminders.php и увидить какие ошибки могут выводиться. Для смены локализации на, допустим, русский язык вам понадобится изменить в файле app/config/app.php значение locale с en на ru и добавить папку app/lang/ru, в которой воссоздать файлы как в папке app/lang/en.

Далее добавим 4 маршрута:

// 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'));
});
...

Для перехода на восстановление так же добавим ссылку на странице логина:

// app/views/login/index.blade.php
...
{{ Form::close() }}

<p>{{ link_to_route('password.remind', 'Forgot password?') }}</p>
...

А так же недостающие методы в 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.');
	    });
	}

Теперь любой пользователь может восстановить свой пароль.

Добавим еще ссылку для входа и регистрации на сайт на главной странице:

// 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>
...

Для того, что бы ограничить вывод на страницах только тех скидок, которые еще не закончились нам понадобится добавить еще один метод в Модель 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);
	}
	...

Таким образом, мы можем в методе HomeController@index всего лишь изменить Offer::orderBy('created_at', 'desc')->get() на Offer::active()->sortLatest()->get(). Наш новосозданный метод будет добавлять в цепочку условий нужные нам условия. Сделаем так же для методов сортировки по тегам, городам и компаниям.

// app/controllers/HomeController.php
	...
	public function byTag($name)
	{
		...
		$offers = $tag->offers()->active()->sortLatest()->get();
		...
	}
Пагинация

Немаловажным аспектом является пагинация. Да, конечно можно слать запросы в БД, получать тысячи строк ответов, и потом их все пихать на страницу. Но это вряд ли чей либо подход. Ограничить количество возвращаемых результатов из БД достаточно просто — в конце запроса нужно использовать метод paginate() вместо get(), или all(). Простой пример:

// 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
...

Таким образом на одной странице будут выводиться только 15 результатов, и внизу будут переходы по страницам. Количество результатов легко изменяемо — достаточно передать нужное число в метод, например paginate(1) даст 1 результат на страницу.

// 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'));
	}
	...

Ничего вроде сложного в этом нет.

Для удобства так же сделаем и в админ панели.

// 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'));
	}
	...

Последнее, что хочется добавить к сайту — так это вывод последних комментариев на страницах и закладки из скидок, к которым пользователь оставил комментарии.

Начнем с добавления комментариев в каркасе страницы:

// 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>

А так же создадим сам шаблон 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>

Не забываем добавлять связь между Моделью Comment User и 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("rn", '<br>', e($str));
		return $str;
	}
	...

А так же вспомогательная функция для сокращения и избавлением от html-тегов комментария.

Осталось добавить закладки для пользователя:

// 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'));
	}
	...

Для начала мы добавили маршрут в app/route.php, потом добавили ссылку на него в app/views/layouts/main.blade.php, задали связь между Моделью User и Offer, а в конце реализовали метод bookmarks в HomeController.

Деплой

Настал час деплоя! Для этого я выбрал fortrabbit.com — хостинг для приложений на PHP. Он поддерживает Git, SSH, Memcached, Composer, MySQL и другое.

Процес регистрации там довольно прост.

Далее создаем новое приложение.

Назовем его habr. Именем проекта будет ссылка на него habr.eu1.frbit.net/. Добавим заметку (Habra Offers), и добавим ssh ключ со своей машины. Чтобы посмотреть свой ssh ключ введите в терминале:

cat ~/.ssh/id_rsa.pub

Последним этапом будет ожидание конфигурации окружения. Вам сформируются данные для доступа к репозиторию Git, SSH и SFTP, MySQL настройки и ReSync доступ.

Окружение запущено и работает.

fortrabbit замораживает не активные приложения. То, как разморозить приложение можно почитать тут.
Теперь для того, чтобы залить наше приложение на fortrabbit идем в терминал:

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

Будет создан клон пустого репозитория с fortrabbit‘a. Далее просто перенесем весь проект с папки workspace/php/habr в папку workspace/php/fort_habr. Зайдем в файл конфигурации БД и исправим на новые данные MySQL. Теперь мы готовы заливать наше приложение:

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

После всего, осталось зайти через ssh и запустить миграции. Итак:

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

Потом введите свой пароль и вы на сервере.
Перейдите в папку htdocs и выполните:

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

Если настройка БД была правильной — никаких проблем возникнуть не должно.

Для работы с Composer на хостинге можно даже не использовать ssh — достаточно в коммите добавить такой триггер:

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

Опция --allow-empty здесь для того, чтобы мы могли запустить сделать коммит, не внося каких-либо изменений в файлах. Как бы пустой коммит. Но увидев в комментарии [trigger:composer:update], хостинг автоматически запустит команду composer update, и все зависимости проекта будут обновлены.

Кстати, в своем репозитории на GitHub я добавил еще seeds и картинки для скидок.

И последнее: прежде, чем переходить на свой сайт убедитесь, что в Domains на сервере Root Path соответсвует значению public. Так как именно таким образом устроен Laravel.

Поиграться можно тут: Habra Offers.

Заключение

Надеюсь вам было интересно это читать, и полезно это делать. Laravel — отличный фреймворк для разработки веб приложений разной сложности.

Основные, и даже больше, аспекты я постарался объяснить. И для интереса дам домашнее задание:

  • Добавьте в главное меню ссылку, чтобы можно было посмотреть только те предложения, которые истекают в течении недели/дня.
  • Добавьте в админку блокировку комментариев, чтобы они скрывались в списке комментариев.
  • Добавьте подсчет оценок для скидки (средняя оценка).
  • Добавьте пакет по управлению изображений.
  • Добавьте возможность пользователю заливать свою аватарку.
  • Добавьте WYSIWYG редактор в админке.

Пожалуй неплохие таски, как считаете?

Об авторе

  • Мне 24 года, женат.
  • Первое высшее: УЭП «КРОК». Специальность: Международная Экономика, магистр.
  • На данный момент студент 3 курса НТУУ КПИ, Факультет Прикладной Математики. Специальность: Программная Инженерия.
  • Работаю веб-разработчиком 15 месяцев на пол ставки.
  • Изучаю Laravel с версии 3.
Сбор статистики

  • На написание статьи с разработкой ушло чуть больше недели.
  • Статья содержит 3040 строк (в текстовом редакторе).
  • Статья содержит 100500 символов (в текстовом редакторе).

Все грамматические ошибки пишите, пожалуйста в личку.

Haters gonna die (Поспорил, что напишу это).

UPD: Полезные ссылки

  • Laravel Documentation
  • Laravel Cheat Sheet — спасибо yTko
  • Laravel 4 Generators
  • Сообщество в ВК, где тоже много ресурсов для обучения

Понравилась статья? Поделить с друзьями:
  • Описание руководства предприятия
  • Карамзин таблетки инструкция по применению взрослым от чего
  • Как заехать на эстакаду на механике на площадке пошаговая инструкция
  • Левофлоксацин 500 инструкция отзывы при цистите
  • Газовая колонка термет инструкция по эксплуатации