Конспект разработки приложения на Laravel
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
app
bootstrap
docs
public
.gitattributes
.gitignore
CONTRIBUTING.md
_ide_helper.php
artisan
composer.json
env.local.php
phpunit.xml
readme.md
server.php

readme.md

Achievements

Requirements

  • PHP >= 5.4 + Mcrypt
  • Mysql
  • Composer
  • Git (для установки готового приложения)

Вы можете настроить Homestead -- виртуальную машину, которая позволит вам на любой системе (Windows, Mac, Linux) развернуть девелоперское окружение, включающее Ubuntu 14.04, Nginx, MySQL, PostgreSQL, Redis, Memcached и многое другое. Причём вам даже не придётся самим настраивать сервер, а добавление новых сайтов происходит добавлением двух строчек в конфигурационном файле. Подробнее см. https://github.com/boxfrommars/ach/blob/master/docs/homestead.md

Установка готового приложения

xu@calypso:~$ git clone https://github.com/boxfrommars/achievements-laravel.git
xu@calypso:~$ cd achievements-laravel/
xu@calypso:~$ composer update
xu@calypso:~$ chmod a+rw app/storage -R # папка для хранения логов, кеша и всего такого

# создаём бд (если изменили здесь параметры бд, то меняем их в кофигурации в файле app/config/database.php)
mysql> CREATE USER 'ach'@'localhost' IDENTIFIED BY 'ach';
mysql> CREATE DATABASE ach;
mysql> GRANT ALL PRIVILEGES ON ach . * TO 'ach'@'localhost';
mysql> FLUSH PRIVILEGES;

xu@calypso:~$ php artisan migrate
xu@calypso:~$ php artisan db:seed # тестовые данные, чтобы обновить миграции и данные: php artisan migrate:refresh --seed

xu@calypso:~$ php artisan serve --port 8444 # запускаем сервер

Разработка приложения

Для начала создадим с помощью composer проект, и дадим серверу права на запись и чтение папки app/storage

xu@calypso:~$ composer create-project laravel/laravel ach --prefer-dist # создаём проект
xu@calypso:~$ cd ach # переходим в папку проекта
xu@calypso:~/ach$ composer update
xu@calypso:~/ach$ chmod a+rw app/storage -R # права на чтение и запись для сервера. можно (и для продакшна -- нужно) просто разрешить для группы вебсервера

Теперь для запуска сервера достаточно выполнить (если вы сами настроили apache или nginx или используете Homestead, запускать сервер не нужно)

xu@calypso:~/ach$ php artisan serve # дополнительный параметр --port для указания конкретного порта 

php artisan -- это набор консольных комманд поставляющихся с laravel, облегчающих разработку на laravel, весь список доступных комманд можно посмотерть выполнив php artisan list или на странице документации

commit 4252a94 init

Настраиваем определение окружения разработчика http://laravel.com/docs/configuration#environment-configuration

По умолчанию окружение -- production, а для разработки нам понадобится использовать откружение local. из коробки единственная разница между local и production только в том, что для локал выставлен параметр конфигурации debag => true и вынесен отдельный конфиг для подключения к бд, в котором сконфигурирован доступ к бд по умолчанию в homestead, но мы можем переписать для локального окружения любой параметр, также можно добавлять собственные окружения. Подробнее см. документацию

Для работы в локальном окружении добавить в файле bootstrap/start.php в массиве передаваемом в ->detectEnvironment имя своего компьютера (в Linux/Mac определяется командой hostname)

$env = $app->detectEnvironment(array(
    'local' => array('your-machine-name'),
));

Настраиваем автодополнение https://github.com/barryvdh/laravel-ide-helper

Так как в laravel используются 'фасады' (подробнее), то в вашей ide не будет работать автодополнение, а это значит, что вас ждут вечные муки. Благо есть пакет, который решает эту проблему. установим его

xu@calypso:~/ach$ composer require barryvdh/laravel-ide-helper:1.* # добавляем пакет для генерации файлов для автодополнения

Теперь добавим в массив провайдеров в файле app/config/app.php следующую строчку

'Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider'

Подробнее о сервис-провайдерах см. http://laravel.com/docs/ioc#service-providers

теперь мы можем генерировать файл-хелпер для автодополнения, с помощью команды artisan: php artisan ide-helper:generate

xu@calypso:~/ach$ php artisan clear-compiled
xu@calypso:~/ach$ php artisan ide-helper:generate # т.к. мы не описали соединение с бд, то выскочит ошибка Could not determine driver/connection for DB -- это нормально
xu@calypso:~/ach$ php artisan optimize

Настраиваем debugbar https://github.com/barryvdh/laravel-debugbar

xu@calypso:~/ach$ composer require barryvdh/laravel-debugbar:dev-master

Теперь добавим в массив провайдеров в файле app/config/app.php следующую строчку

'Barryvdh\Debugbar\ServiceProvider',

Добавим ресурсы этого пакета (стили, js)

xu@calypso:~/ach$ php artisan debugbar:publish

В документации к пакету автор отмечает, что ресурсы могут меняться и советует добавить в ваш composer.json в post-update следующую строчку:

"post-update-cmd": [
    "php artisan debugbar:publish"
],

Commit 8cb95c8 ide helper, debugbar, local enviroment

Настраиваем базу данных

Создаём базу

mysql> CREATE USER 'ach'@'localhost' IDENTIFIED BY 'ach';
mysql> CREATE DATABASE ach;
mysql> GRANT ALL PRIVILEGES ON ach . * TO 'ach'@'localhost';
mysql> FLUSH PRIVILEGES;

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

'mysql' => array(
    'driver'    => 'mysql',
    'host'      => 'localhost',
    'database'  => 'ach',
    'username'  => 'ach',
    'password'  => 'ach',
    'charset'   => 'utf8',
    'collation' => 'utf8_unicode_ci',
    'prefix'    => '',
),

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

xu@calypso:~/ach$ php artisan clear-compiled
xu@calypso:~/ach$ php artisan ide-helper:generate
xu@calypso:~/ach$ php artisan optimize

Commit 9aa9238 Настройка подключения к БД

Создаём миграцию для таблицы users

Для users по умолчанию уже идёт модель User, поэтому создавть вручную её не требуется

Создадим миграцию для создания таблицы users

xu@calypso:~/ach$ php artisan migrate:make create_users_table

создался файл ach/app/database/migrations/YYYY_MM_DD_SSZZZZ_create_users_table.php, описываем в нём миграцию

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration {

    // то что происходит при 'накатывании' миграции, в нашем случае создание таблицы
	public function up()
	{
        Schema::create('users', function(Blueprint $table)
        {
            $table->string('name');
            $table->string('image')->nullable();
            $table->increments('id');
            $table->string('email')->unique();
            $table->string('password');
            $table->string('remember_token', 100)->nullable();
            $table->timestamps(); // стандартные timestamps: created_at, updated_at
            $table->softDeletes(); // 'мягкое' удаление, колонка  deleted_at
        });
	}
	
    // то что происходит при 'откате' миграции, в нашем случае удаление таблицы
	public function down()
	{
        Schema::table('users', function(Blueprint $table)
        {
            $table->drop();
        });
	}
}

отметим использование 'мягкого' удаления, при удалении с помощью $user->delete() сама запись из таблицы не удаляется, а лишь помечается, как удалённая, при этом в исключается из результатов запросов вида User:all() и тому подобных. подробнее см. http://laravel.com/docs/eloquent#soft-deleting

вообще говоря миграции нужны не только для создания таблиц, но и для любых других действий с ними: например, для добавления/удаления колонок, изменения колонок и даже для добавления изменения данных. Подробнее см. http://laravel.com/docs/migrations и https://laracasts.com/index/migration

Применяем миграцию

xu@calypso:~/ach$ php artisan migrate

Создаём модель и миграцию для таблицы achievements

Тут придётся создать модель вручную, то есть создать файл app/models/Achievement.php со следующим содержимым

class Achievement extends Eloquent {
	protected $table = 'achievements'; // в данном случае не обязательно указывать таблицу, так как её имя -- множественное число от имени класса модели и магия laravel всё сделала бы за вас
}

Теперь создаём миграцию точно так же как и для users

xu@calypso:~/ach$ php artisan migrate:make create_achievements_table

создался файл app/database/migrations/YYYY_MM_DD_SSZZZZ_create_achievements_table.php, описываем в нём миграцию

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateAchievementsTable extends Migration {

	public function up()
	{
        Schema::create('achievements', function(Blueprint $table)
        {
            $table->increments('id');

            $table->integer('depth'); // глубина
            $table->integer('outlook'); // кругозор
            $table->integer('interaction'); // взаимодействие

            $table->string('title');
            $table->text('description')->nullable();
            $table->string('image')->nullable();

            $table->timestamps();
        });
	}

	public function down()
	{
        Schema::table('achievements', function(Blueprint $table)
        {
            $table->drop();
        });
	}
}

Применяем миграцию

xu@calypso:~/ach$ php artisan migrate

Создаём связь многие ко многим для таблиц users и achievements

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

xu@calypso:~/ach$ php artisan migrate:make create_user_achievements_table

создался файл app/database/migrations/YYYY_MM_DD_SSZZZZ_create_user_achievements_table.php, описываем в нём миграцию

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUserAchievementsTable extends Migration {

	public function up()
	{
        Schema::create('user_achievements', function(Blueprint $table)
        {
            $table->integer('id_user')->unsigned();
            $table->integer('id_achievement')->unsigned();
            $table->boolean('is_approved')->default(false);

            $table->foreign('id_user')->references('id')->on('users');
            $table->foreign('id_achievement')->references('id')->on('achievements');
        });
	}

	public function down()
	{
        Schema::table('user_achievements', function(Blueprint $table)
        {
            $table->drop();
        });
	}
}

Применяем миграцию

xu@calypso:~/ach$ php artisan migrate

Добавляем в модель Achievement метод ->users()

// User >-< Achievement many to many relationship
public function users()
{
    return $this->belongsToMany('User', 'user_achievements', 'id_achievement', 'id_user');
}

А в модель User метод ->achievements()

// User >-< Achievement many to many relationship
public function achievements()
{
    return $this->belongsToMany('Achievement', 'user_achievements', 'id_user', 'id_achievement')->withPivot('is_approved');
}

Теперь мы можем получать связанные сущности, например:

$user = User::find($id);
$achievements = $user->achievements; // все достижения данного пользователя
$achievement = $achievements[0];
$isApproved = $achievement->pivot->is_approved; // получаем данные из таблицы связи

или

$achievement = Achievement::find($id);
$users = $achievements->users; // все пользователи с данным достижением

Подробнее http://laravel.com/docs/eloquent#relationships

Текущая структура БД: users and achievements

Commit 8ecf5cc users and achievements: models, migrations and relationship

Создаём миграции, модели и связи для сущности Group

xu@calypso:~/ach$ php artisan migrate:make create_groups_table

создался файл app/database/migrations/YYYY_MM_DD_SSZZZZ_create_groups_table.php, описываем в нём миграцию

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateGroupsTable extends Migration {

	public function up()
	{
        Schema::create('groups', function(Blueprint $table)
        {
            $table->increments('id');
            $table->string('title');
            $table->string('description');
            $table->string('code');
            $table->string('image')->nullable();
            $table->timestamps();
        });
	}

	public function down()
	{
        Schema::table('groups', function(Blueprint $table)
        {
            $table->drop();
        });
	}
}

создаём модель app/models/Group.php

class Group extends Eloquent {
}

Создаём связь многие ко многим между сущностями Group и User

xu@calypso:~/ach$ php artisan migrate:make create_user_groups_table

создался файл app/database/migrations/YYYY_MM_DD_SSZZZZ_create_user_groups_table.php, описываем в нём миграцию

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUserGroupsTable extends Migration {

	public function up()
	{
        Schema::create('user_groups', function(Blueprint $table)
        {
            $table->integer('id_user')->unsigned();
            $table->integer('id_group')->unsigned();

            $table->foreign('id_user')->references('id')->on('users');
            $table->foreign('id_group')->references('id')->on('groups');
        });
	}
	public function down()
	{
        Schema::table('user_groups', function(Blueprint $table)
        {
            $table->drop();
        });
	}
}

Применяем миграции

xu@calypso:~/ach$ php artisan migrate

Добавляем в модель Group метод ->users()

// User >-< Achievement many to many relationship
public function users()
{
    return $this->belongsToMany('User', 'user_groups', 'id_group', 'id_user');
}

А в модель User метод ->groups()

// User >-< Group many to many relationship
public function groups()
{
    return $this->belongsToMany('Group', 'user_groups', 'id_user', 'id_group');
}

Создаём связь многие ко многим между сущностями Group и Achievement

xu@calypso:~/ach$ php artisan migrate:make create_achievement_groups_table

создался файл app/database/migrations/YYYY_MM_DD_SSZZZZ_create_achievement_groups_table.php, описываем в нём миграцию

Добавляем в модель Achievement метод ->groups()

// Achievement >-< Group many to many relationship
public function groups()
{
    return $this->belongsToMany('Group', 'achievement_groups', 'id_achievement', 'id_group');
}

А в модель Group метод ->achievements()

// Group >-< Achievement many to many relationship
public function achievements()
{
    return $this->belongsToMany('Achievement', 'achievement_groups', 'id_group', 'id_achievement');
}

Текущая структура БД: groups

Теперь мы можем перегенерировать хелпер-файл для поддержки автодополнения в созданных моделей, для этого используется команда php artisan ide-helper:models

xu@calypso:~/ach$ php artisan clear-compiled
xu@calypso:~/ach$ php artisan ide-helper:generate
xu@calypso:~/ach$ php artisan optimize

генератор спросит, добавлять ли докблок с методами и свойствами в каждый файл модели или описать модели в стандартном файле _ide_helper.php, я предпочитаю добавлять в модели.

Записываем тестовые данные

Добавляем папку publig/img/user для хранения аватарок пользователей

Добавляем папку publig/img/achievement для хранения картинок достижений

Кладём в каждую из этих папок файл .gitignore (чтобы картинки не попадали в репозиторий, но сами папки создавались) со следующим содержимым

*
!.gitignore

Добавляем очень удобный пакет для генерации тестовых данных (документация)

xu@calypso:~/ach$ composer require fzaninotto/faker:1.4.*@dev

Создаём файл app/database/seeds/AchievementSeeder.php

<?php

class AchievementSeeder extends Seeder
{
    /** @var \Faker\Generator */
    protected $_faker;

    public function __construct()
    {
        $this->_faker = Faker\Factory::create('ru_RU');
    }

    public function run()
    {
        $usersCount = 16;
        $achievementsCount = 10;
        $beardFrequency = 4; // на сколько мальчишек один бородач
        $defaultPassword = '123123';

        // а вот и все группы
        $groupsData = array(
            array('title' => 'мальчишки', 'code' => 'male'),
            array('title' => 'девчонки', 'code' => 'female'),
            array('title' => 'разработчики', 'code' => 'developer'),
            array('title' => 'дизайнеры', 'code' => 'designer'),
            array('title' => 'менеджеры', 'code' => 'manager'),
            array('title' => 'бородачи', 'code' => 'beard'),
        );

        $userImageDirectory = 'public/img/user/';
        $achievementImageDirectory = 'public/img/achievement/';

        // Удаляем предыдущие данные
        DB::table('user_groups')->delete();
        DB::table('achievement_groups')->delete();
        DB::table('user_achievements')->delete();
        DB::table('groups')->delete();
        DB::table('achievements')->delete();
        DB::table('users')->delete();

        $this->_cleanImageDirectory($userImageDirectory);
        $this->_cleanImageDirectory($achievementImageDirectory);

        /** @var Group[] $groups */
        $groups = array();

        foreach ($groupsData as $group) {
            $groups[$group['code']] = Group::create(array(
                'title' => $group['title'],
                'description' => '',
                'code' => $group['code'],
            ));
        }

        /** @var Achievement[] $achievements */
        $achievements = array();

        for ($i = 0; $i < $achievementsCount; $i++) {
            $achievement = Achievement::create(array(
                'depth' => $this->_faker->numberBetween(0, 100),
                'outlook' => $this->_faker->numberBetween(0, 100),
                'interaction' => $this->_faker->numberBetween(0, 100),

                'title' => $this->_faker->sentence(3),
                'description' => $this->_faker->paragraph(),
                'image' => $this->_faker->image($achievementImageDirectory, 100, 100, 'abstract', false),
            ));

            $achievement->groups()->sync($this->_getRandomIds($groups));
            $achievements[] = $achievement;
        }

        // Добавляем администратора
        /** @var User $user */
        $userData = array(
            'name' => 'Dmitry Groza',
            'email' => 'boxfrommars@gmail.com',
            'password' => Hash::make($defaultPassword), // см. http://laravel.com/docs/security#storing-passwords
            'image' => $this->_faker->image($userImageDirectory, 100, 100, 'people', false),
        );
        $userGroupIds = array($groups['developer']->id, $groups['male']->id);
        $userAchievementIds = $this->_getRandomIds($achievements, 4);

        $this->_createUser($userData, $userGroupIds, $userAchievementIds);

        // Добавляем остальных тестовых пользователей
        for ($i = 0; $i < $usersCount; $i++) {

            $gender = $this->_faker->randomElement(array('male', 'female'));

            /** @var User $user */
            $userData = array(
                'name' => mb_convert_case($this->_faker->name($gender), MB_CASE_TITLE), // у фейкера нехорошие имена/фамилии, то с большой буквы, то с маленькой. приводим к нормальному виду
                'email' => $this->_faker->email,
                'password' => Hash::make($defaultPassword),
                'image' => $this->_faker->image($userImageDirectory, 100, 100, 'people', false),
            );

            $position = $this->_faker->randomElement(array('developer', 'manager', 'designer'));
            $userGroupIds = array($groups[$gender]->id, $groups[$position]->id);
            $userAchievementIds = $this->_getRandomIds($achievements, 4);

            // добавляем немного бородачей
            if ($gender === 'male' && rand(1, $beardFrequency) === 1) {
                array_push($userGroupIds, $groups['beard']->id);
            }

            $this->_createUser($userData, $userGroupIds, $userAchievementIds);
        }
    }

    /**
     * @param array $data данные, которые прямиком отправляются в User::create($data)
     * @param array $groupIds массив id групп
     * @param array $achievementIds массив id достижений
     */
    protected function _createUser($data, $groupIds, $achievementIds)
    {
        $user = User::create($data);
        $user->groups()->sync($groupIds);

        if (!empty($achievementIds)) { // тут, в отличии от groups нужна проверка, т.к. array_fill вторым параметром  принимает только integer > 0
            $user->achievements()->sync(
                array_combine($achievementIds, array_fill(0, count($achievementIds), array('is_approved' => true)))
            );
        }
    }

    /**
     * @param string $directory директория для очищения
     *
     * очищаем директорию, при этом не удалянм в ней файл .gitignore
     */
    protected function _cleanImageDirectory($directory)
    {
        if (File::isDirectory($directory)) {
            $items = new FilesystemIterator($directory);
            foreach ($items as $item) {
                if (!$item->isDir() && $item->getFilename() !== '.gitignore') {
                    File::delete($item->getRealPath());
                }
            }
        }
    }

    /**
     * @param Eloquent[] $entities массив объектов со свойством id
     * @param integer    $maxCount максимальное число возвращаемых id
     * @return array массив id случайно выбранных объектов из списка
     */
    protected function _getRandomIds($entities, $maxCount = null)
    {
        if ($maxCount === null) {
            $maxCount = count($entities);
        }

        return array_map(
            function ($item) {
                return $item->id;
            },
            $this->_faker->randomElements($entities, rand(1, $maxCount))
        );
    }
}

а в файле app/database/seeds/AchievementSeeder.php добавляем вызов генерации тестовых данных

$this->call('AchievementSeeder');

Commit b9c4bc7 test data

Роутинг и контроллеры

На данный момент нужно реализовать следующие пути

  • / список достижений
  • /users список пользователей
  • /users/{id} страница пользователя, где id -- идентификатор пользователя
  • /achievements тоже список достижений (?)
  • /achievements/{id} страница достижения

в файле app/routes.php удаляем текущий роут для пути / и прописываем наши роуты

Route::get('/', 'AchievementController@getMain');

Route::get('users', 'AchievementController@getUsers');
Route::get('users/{id}', 'AchievementController@getUser');

Route::get('achievements', 'AchievementController@getAchievements');
Route::get('achievements/{id}', 'AchievementController@getAchievement');

Так как страниц не очень много, то все их заносим в один контроллер AchievementController

Создаём файл app/controllers/AchievementController.php со следующим содержимым (если действие контроллера возвращает массив, то приложение возвращает ответ в формате json с соответствующим хедером application/json, при это eloquent модели тоже корректно преобразовываются, с исключенными hidden полями, которые мы установили в соответствующей модели, как,например, поле password)

<?php

class AchievementController extends BaseController
{

    public function getMain()
    {
        return ['url' => '/'];
    }

    public function getUsers()
    {
        $users = User::all();

        return $users;
    }

    public function getUser($id)
    {
        $user = User::find($id);
        if ($user === null) {
            App::abort(404, 'Page not found');
        }

        return $user;
    }

    public function getAchievements()
    {
        $achievements = Achievement::all();

        return $achievements;
    }

    public function getAchievement($id)
    {
        $achievement = Achievement::find($id);
        if ($achievement === null) {
            App::abort(404, 'Page not found');
        }

        return $achievement;
    }
}

Теперь можно открыть соответствующие страницы в браузере и убедиться, что всё работает.

Commit 4ef320b routing & controllers

Добавляем виды

Сначала создадим общий лайаут app/views/layout.blade.php, в котором в месте, где будет выводиться контент вставляем @yield('content') (см. документацию к шаблонизотру blade). Заодно удалим ненужные нам app/views/hello.php и app/views/email.

...
<!-- Begin page content -->
<div class="container">
    @yield('content')
</div>
...

Теперь создадим отдельные страницы (и соответствующие папки) для каждого действия

app/views/user/user_list.blade.php

@extends('layout')

@section('content')
    <h1>Пользователи</h1>
    @foreach ($users as $user)
    <div class="media">
        <div class="pull-left">
            <img class="img-thumbnail" src="/img/user/{{{ $user->image }}}" />
        </div>
        <div class="media-body">
            <h4 class="media-heading"><a href="/users/{{{ $user->id }}}">{{{ $user->name }}}</a></h4>
            <ul class="list-inline">
                @foreach ($user->achievements as $achievement)
                <li><a href="/achievements/{{{ $achievement->id }}}" title="{{{ $achievement->title }}}"><img class="img-thumbnail achievement-image-icon" src="/img/achievement/{{{ $achievement->image }}}" /></a></li>
                @endforeach
            </ul>

            <ul class="list-inline">
                @foreach ($user->groups as $group)
                <li><a class="label label-group label-{{{ $group->code }}}" href="/groups/{{{ $group->id }}}">{{{ $group->title }}}</a></li>
                @endforeach
            </ul>
        </div>
    </div>
    @endforeach
@stop

По аналогии создадим виды app/views/user/user_show.blade.php, app/views/achievement/achievement_show.blade.php, app/views/achievement/achievement_list.blade.php

Изменим контроллер app/controllers/AchievementController.php, чтобы он начал работать с созданными видами:

<?php

class AchievementController extends BaseController
{

    public function getMain()
    {
        return View::make('layout');
    }

    public function getUsers()
    {
        $users = User::all();

        return View::make('user.user_list', array('users' => $users));
    }

    public function getUser($id)
    {
        $user = User::find($id);
        if ($user === null) {
            App::abort(404, 'Page not found');
        }

        return View::make('user.user_show', array('user' => $user));
    }

    public function getAchievements()
    {
        $achievements = Achievement::all();

        return View::make('achievement.achievement_list', array('achievements' => $achievements));
    }

    public function getAchievement($id)
    {
        $achievement = Achievement::find($id);
        if ($achievement === null) {
            App::abort(404, 'Page not found');
        }

        return View::make('achievement.achievement_show', array('achievement' => $achievement));
    }
}

Теперь настало время заглянуть в наш debugbar.

А там мы увидим, что страница /users делает 35 (жуть) запросов (при 16 пользователях) к базе данных. Дело в том, что каждый раз, когда мы обращаемся к связанным сущностям eloquent-модели, выполняется запрос к базе, получающий эти связанные сущности. Но это легко исправить, достаточно заменить:

  • User::all() на User::with('achievements', 'groups')->get()
  • Achievement::all() на Achievement::with('users', 'groups')->get()
  • Achievement::find($id) на Achievement::with('users', 'groups')->find($id) (при запросе одной моделей, нет плюсов в использовании with -- и так и так выполняются три запроса, но это понадобится нам чуть ниже)
  • User::find($id) на User::with('achievements', 'groups')->find($id)

и мы получим всего три запроса (для users: выборка всех пользователей, выборка всех групп этих пользователей и выборка всех достижений этих пользователей)

Теперь открываем страницу /achievements/{id} и видим, что даже после замены выполняются десятки запросов. Это понятно, мы подгрузили только связанных пользователей, но не их связанные группы и достижения, поэтому для каждого пользователя в списке, будет выполняться ещё по два запроса. И кажется, что вот тут-то всё кончено и придётся писать что-то громоздкое собственными руками. Но это не так :) В laravel и для этого есть немного магии, а именно вот такая конструкция:

Achievement::with('users.groups', 'user.achievements', 'groups')->find($id)

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

Commit 48cec17

Аутентификация

По умолчанию с laravel уже идёт модель User (app/models/User.php), которая довольно просто используется для аутентификации с помощью
Eloquent драйвера аутентификации. Также мы выше уже создали таблицу users со всеми необходимыми полями. Теперь для аутентификации пользователя, нам достаточно в контроллере, в котором будет происходить логин, проверить (предварительно, конечно, полчив $email и $password из формы):

if (Auth::attempt(array('email' => $email, 'password' => $password)))
{
    // успех
} else {
    // неудача
}

мы используем колонку email для аутентификации, но вы может использовать и другую колонку, например, username

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

class AuthController extends BaseController
{
   public function getLogin()
   {
       return View::make('auth.login');
   }

   public function postLogin()
   {
       // валидатор проверяющий заполнены ли поля формы
       $validator = Validator::make(Input::all(), array(
           'email' => 'required',
           'password' => 'required'
       ));

       $credentials = array(
           'email' => Input::get('email'),
           'password' => Input::get('password'),
       );

       // если Auth::attempt вторым параметром принимает true, то приложение запоминает пользователя на неопределённое время
       // подробнее см. http://laravel.com/docs/security#authenticating-users
       $isRemember = Input::get('is_remember');

       if ($validator->passes() && Auth::attempt($credentials, $isRemember)) {
           // в случае успешной аутентификации редиректим на главную
           return Redirect::intended($path);
       } else {
           // в случае неуспешной -- редиректим назад на форму, заполняя поля введёнными данными, также записываем в flash-сообщение ошибки
           return Redirect::back()
               ->withInput()
               ->with('errors', array('Неправильный логин или пароль'));
       }
   }

   public function logout()
   {
       Auth::logout();

       return Redirect::to('/');
   }
}

Input::all(), Input::get($fieldName) введённые пользователем данные, см. http://laravel.com/docs/requests#basic-input Validator::make -- валидатор для данных, см. http://laravel.com/docs/validation#basic-usage Redirect::back(), Redirect::to($path), Redirect::intended($path) -- редиректы, см. http://laravel.com/docs/responses#redirects, о Redirect::intended($path)будет написано чуть ниже

Теперь создадим вид для страницы логина auth/login.blade.php (точно так же, как и раньше наследуемся от общего лайаута и переписываем секцию content), подробнее о работе с формами см. http://laravel.com/docs/html

@extends("layout")

@section("content")

<h3>Вход</h3>

{{ Form::open(array('role' => 'form', 'class' => 'form-horizontal')) }}
<div class="form-group">
    {{ Form::label("email", "Email", array('class' => 'col-sm-2 control-label')) }}
    <div class="col-sm-4">
        {{ Form::text("email", Input::old("email"), array('class' => 'form-control')) }}
    </div>
</div>
<div class="form-group">
    {{ Form::label("password", "Пароль", array('class' => 'col-sm-2 control-label')) }}
    <div class="col-sm-4">
        {{ Form::password("password", array('class' => 'form-control')) }}
    </div>
</div>
<div class="form-group">
    <div class="col-sm-offset-2 col-sm-4">
        <div class="checkbox">
            <label>
                <input type="checkbox" name="is_remember"> Запомнить меня
            </label>
        </div>
    </div>
</div>
<div class="form-group">
    <div class="col-sm-offset-2 col-sm-4">
        {{ Form::submit("Войти", array('class' => 'btn btn-default')) }}
    </div>
</div>
{{ Form::close() }}

@stop

И добавим в app/routes.php:

Route::get('login', 'AuthController@getLogin');
Route::post('login', 'AuthController@postLogin');
Route::get('logout', 'AuthController@logout');

Также изменим наш общий лайаут, добавив функциональность для вывода любых ошибок переданных во флеш-сообщениях (см. http://laravel.com/docs/session#flash-data и http://laravel.com/docs/responses#redirects). Для этого добавим

@if (Session::has('errors'))
    @foreach (Session::get('errors') as $error)
        <div class="alert alert-danger">{{ $error }} <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button></div>
    @endforeach
@endif

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

<ul class="nav navbar-nav navbar-right">
    @if (Auth::check())
        <li><a href="/my">Мои успехи</a></li>
        <li><a href="/logout">Выйти</a></li>
    @else
        <li><a href="/login">Войти</a></li>
    @endif
</ul>

Auth::check() -- проверяет, прошёл ли человек аутентификацию

Теперь можно устроить страницу /my пользователя, воспользовавшись методом Auth::user(). Добавим соответствующий метод в AchievementController

public function getMy()
{
    /** @var User $user */
    $user = Auth::user();
    if (is_null($user)) App::abort(404, 'Page not found');

    // воспользуемся тем же видом, что и для страницы пользователя
    return View::make('user.user_show', array('user' => $user));
}

И добавим новый роут

Route::get('my', array('before' => 'auth', 'uses' => 'AchievementController@getMy'));

Тут мы воспользовались встроенным фильтром auth (находится в файле app/filters.php), который проверяет выполнил ли пользователь вход и, если нет, запишет в сессию, адрес текущей страницы и перенаправит пользователя на страницу входа. После входа пользователя перенаправит обратно на данный адрес. Для этого используется метод Redirect::intended($path) (см. AuthController@postLogin), который в случае существования в сессии intended страницы, перенаправит на неё или на $path в другом случае.