Уже достаточно давно в свободное время внедряю в рабочие процессы фреймворк Laravel 5. Свободного времени обычно очень мало, но дело потихонечку движется. В данной заметке я покажу, как на этом самом Laravel 5 реализовать систему регистрации через инвайты (сиречь приглашения). То есть, когда с инвайтом регистрироваться можно, а без инвайта — нельзя.
Перед разработкой я внимательно прочитал статью Creating an advanced Invitation System in Laravel 4. В ней описано почти то же самое для Laravel 4, так что рекомендую ознакомиться. Главные отличия этой заметки:
- здесь используется Laravel 5;
- для ограничения доступа к форме регистрации я вместо фильтра применил middleware (он отлично для этого подходит);
- для валидации email-ов, вместо разработки отдельного класса-валидатора, используется трейт ValidateRequests.
Что необходимо для реализации инвайтов:
- Регистрация — чтобы сделать регистрацию через инвайты, нужно чтобы была принципиальная возможность зарегистрироваться. В стандартной инсталляции Laravel 5 эта возможность уже есть, для наших целей ее достаточно.
- Модель для работы с инвайтами и таблица для их хранения.
- Форма для создания инвайтов пользователями.
- Возможность отправить инвайт на указанный почтовый адрес.
- Процедура проверки инвайта для допуска к форме регистрации.
- Сохранение инвайта как отработанного при регистрации (чтобы избежать повторного использования).
Начнем с таблицы для хранения инвайтов. Создадим новую миграцию в database/migrations:
php artisan migrate:make create_invites_table
Код миграции:
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateInvitesTable extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('invites', function(Blueprint $table)
{
$table->increments('id');
$table->integer('inviter_id');
$table->integer('invitee_id');
$table->string('code');
$table->string('email');
$table->timestamp('claimed')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('invites');
}
}
Мы описали таблицу invites для хранения инвайтов. У таблицы есть следующие столбцы:
- id инвайта (автоинкремент);
- id приглашающего пользователя;
- id приглашенного пользователя;
- код — строка с кодом инвайта;
- email — строка с электронным адресом, на который выслан инвайт;
- claimed — момент времени, когда инвайт использован;
- стандартные столбцы created_at и updated_at через timestamps().
namespace App;
use Illuminate\Database\Eloquent\Model;
use Mail;
class Invite extends Model {
//пользователем заполняется только поле email
protected $fillable = ['email'];
/**
*
* Метод вернет объект инвайта по коду и только если инвайт не использован
* Иначе вернет NULL
*
* @param string $code
* @return App\Invite
*/
public static function getInviteByCode($code) {
return Invite::where('code', $code)->where('claimed', NULL)->first();
}
/**
* Генератор рандомного кода инвайта
*/
protected function generateInviteCode() {
$this->code = bin2hex(openssl_random_pseudo_bytes(16));
}
/**
* Метод отправляет инвайт по почте
*
* @param string $message_text - текст сообщения
*/
public function sendInvitation($message_text) {
$inviter = User::find($this->inviter_id);
$params = [
'inviter' => $inviter->name,
'message_text' => (!empty($message_text)) ? $message_text : '',
'code' => $this->code,
];
Mail::send('emails.invite', $params, function ($message) {
$message->to($this->email)->subject('Invite to site');
});
}
/**
*
* Связываем модель инвайта с моделью пользователя, отправляющего приглашение
* Связь через поле inviter_id
*
* @return type
*/
public function inviter() {
return $this->belongsTo('App\User', 'inviter_id');
}
/**
*
* Связываем модель инвайта с моделью пользователя, получившего приглашение
* Может пригодиться, если захотим показывать, кто кого пригласил
* Связь через поле invitee_id
*
* @return type
*/
public function invitee() {
return $this->belongsTo('App\User', 'invitee_id');
}
/**
* Используем метод boot() чтобы подключиться к событию создания модели
* будем генерировать код инвайта сразу при создании
*/
protected static function boot() {
parent::boot();
static::creating(function ($model) {
$model->generateInviteCode();
});
}
}
Обратите внимание, что метод Mail::send()
, который мы используем для отправки инвайта приглашенному пользователю, первым аргументом принимает view, то есть шаблон html-кода письма. А вторым аргументом идет массив параметров подстановки в шаблон. Раз нужен view, давайте сразу его сделаем. Файл resources/views/emails/invite.blade.php:
<p>Hello! {{ $inviter }} invites you to join our site.</p>
@if ($message_text != '')
<p>
He also sends you following message:<br>
{{ $message_text }}
</p>
@endif
<p>
You can join our site by clicking the following link:
<a href="{{ url('auth/register/?code='$code) }}">{{ url('auth/register/?code=' . $code) }}</a>
</p>
Как мы видим из последнего абзаца шаблона, ссылка, по которой приглашенный пользователь сможет зарегистрироваться, будет иметь вид /auth/register/?code=кодинвайта.Подробнее об отправке писем через фасад Mail можно прочитать в документации. Кстати, чтобы отправка писем работала, не забудьте настроить ее в файле config/mail.php.
Теперь, когда у нас есть модель, дадим пользователям возможность создавать инвайты. Для этого нам понадобится форма (создадим файл resources/views/invite/create.blade.php):
@extends('app')
@section('content')
<div class="container-fluid">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">Invite new user</div>
<div class="panel-body">
@if (count($errors) > 0)
<div class="alert alert-danger">
<strong>Whoops!</strong> There were some problems with your input.<br><br>
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
{!! Form::open() !!}
<div class="form-group">
{!! Form::label('email', 'Email:') !!}
{!! Form::text('email', NULL, ['class' => 'form-control']) !!}
{!! Form::label('message', 'Message:') !!}
{!! Form::textarea('message', NULL, ['class' => 'form-control']) !!}
</div>
<div class="form-group">
{!! Form::submit('Send', ['class' => 'btn btn-primary form-control']) !!}
</div>
{!! Form::close() !!}
</div>
</div>
</div>
</div>
</div>
@endsection
Это обычная форма, в которой есть поле для ввода почты, многострочное поле для ввода сопроводительного текста и кнопка отправки. Ну и немного валидации, конечно.
Теперь создадим контроллер, показывающий и сохраняющий эту форму (php artisan make:controller InviteController). Файл app/Http/Controllers/InviteController.php:
namespace App\Http\Controllers;use App\Http\Requests;
use App\Http\Controllers\Controller;
use App\Invite;
use Illuminate\Http\Request;
use Auth;
class InviteController extends Controller {
/**
*
* Метод возвращает view с формой создания инвайта
*
*/
public function create() {
return view('invite.create');
}
/**
* Метод обрабатывает сохранение формы инвайта
*
* @param Request $request
*/
public function store(Request $request) {
//валидируем поле email: поле обязательное, с проверкой синтаксиса email,
//к тому же уникальное по таблицам invites и users (столбец email)
$this->validate($request, ['email' => 'required|email|unique:invites,email|unique:users,email']);
$email = $request->get('email');
$message = $request->get('message');
//создадим новый инвайт и заполним поля email и id отправителя
$inviter = Auth::user();
$invite = new Invite(['email' => $email]);
$invite->inviter_id = $inviter->id;
$invite->save();
//вызовем метод отправки приглашения, реализованный в модели Invite
$invite->sendInvitation($message);
//выведем на экран сообщение, что инвайт успешно отправлен
\Session::flash('status_message', 'Invite has being created and sent to ' .$email);
//и вернем пользователя на страницу с формой инвайта
return redirect('invite');
}
/**
* Метод возвращает view для тех, у кого нет инвайта
*/
public function invitesonly() {
return view('invite.invitesonly');
}
/**
* Конструктор
* - добавим Auth middleware, который позволяет нам легко и просто запретить доступ
* неавторизованным пользователям
* - и тут же добавим исключение: метод invitesonly() должен быть доступен всем.
*/
public function __construct() {
$this->middleware('auth', ['except' => 'invitesonly']);
}
}
Осталось немножко. Создадим middleware, который будет автоматически проверять наличие и корректность инвайта при попытке открыть форму регистрации (php artisan make:middleware InviteMiddleware). Файл app/Http/Middleware/InviteMiddleware.php выглядит так:
namespace App\Http\Middleware;use Closure;
use App\Invite;
class InviteMiddleware {
/**
* Метод обрабатывает входящий запрос
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next) {
//если в запросе не передан параметр 'code', перенаправляем пользователя
if (!$request->has('code')) {
return redirect('auth/invite-only');
}
$code = $request->input('code');
//получаем инвайт - метод вернет объект только если он существует и еще не использован
$invite = Invite::getInviteByCode($code);
//если объект не получен, перенаправляем пользователя
if (!$invite) {
return redirect('auth/invite-only');
}
//все в порядке, продолжаем обработку запроса
return $next($request);
}
}
Не забудем в app/Http/Kernel.php зарегистрировать наш новый middleware в $routeMiddleware:
//...
protected $routeMiddleware = [
'auth' => 'App\Http\Middleware\Authenticate',
'auth.basic' => 'Illuminate\Auth\Middleware\AuthenticateWithBasicAuth',
'guest' => 'App\Http\Middleware\RedirectIfAuthenticated',
//...
'invite' => 'App\Http\Middleware\InviteMiddleware',
];
Подробнее о работе с middleware можно прочитать в документации.
Подготовим view invite-only, куда мы будем перенаправлять пользователей без инвайтов или с неверными кодами инвайтов. Создадим файл resources/views/invite/invitesonly.blade.php:
@extends('app')
@section('content')
<div class="container-fluid">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">Invite-only</div>
<div class="panel-body">
<div class="alert alert-info">
There's ongoing closed testing.
For now, registration is invite-only.
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
Теперь приведем логику регистрации в соответствие с нашим новым функционалом. Нам нужно:1) Чтобы форма регистрации не появлялась, если код инвайта неверный или отсутствует.
2) Чтобы при регистрации (сохранении нового пользователя) инвайт отмечался как использованный.
Влезем в app/Http/Controllers/Auth/AuthController.php — это контроллер, отвечающий за авторизацию и регистрацию новых пользователей:
namespace App\Http\Controllers\Auth;use App\Http\Controllers\Controller;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\Registrar;
use Illuminate\Http\Request;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
use App\Invite;
use App\User;
use Carbon\Carbon;
class AuthController extends Controller {
use AuthenticatesAndRegistersUsers;
/**
* Create a new authentication controller instance.
*
* @param \Illuminate\Contracts\Auth\Guard $auth
* @param \Illuminate\Contracts\Auth\Registrar $registrar
* @return void
*/
public function __construct(Guard $auth, Registrar $registrar) {
$this->auth = $auth;
$this->registrar = $registrar;
$this->middleware('guest', ['except' => 'getLogout']);
//добавим контроллеру наш middleware, но только для функционала регистрации
//то есть для вызовов getRegister и postRegister
$this->middleware('invite', ['only' => ['getRegister', 'postRegister']]);
}
/**
* Переопределяем функцию из трейта AuthenticatesAndRegistersUsers
*
* Handle a registration request for the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function postRegister(Request $request) {
//это код из оригинальной функции трейта
$validator = $this->registrar->validator($request->all());
if ($validator->fails()) {
$this->throwValidationException(
$request, $validator
);
}
$this->auth->login($this->registrar->create($request->all()));
//а этот код мы добавляем:
//получаем объект авторизованного пользователя
//и обновляем данные об инвайте:
// - сохраняем id приглашенного пользователя
// - помечаем инвайт как использованный
$invite = Invite::where('code', $request->input('code'))->first();
$user = $this->auth->user();
$invite->invitee_id = $user->id;
$invite->claimed = Carbon::now();
$invite->save();
return redirect($this->redirectPath());
}
}
Единственное, что осталось — рассказать движку о том, как обработать запрос страницы с формой инвайта, сохранение формы инвайта и запрос страницы auth/invite-only. Добавим в app/Http/routes.php:
Route::get('invite', 'InviteController@create');
Route::post('invite', 'InviteController@store');
Route::get('auth/invite-only', 'InviteController@invitesonly');
Вот и все, теперь наши инвайты должны работать.