Регистрация по инвайтам в Laravel 5

Уже достаточно давно в свободное время внедряю в рабочие процессы фреймворк Laravel 5. Свободного времени обычно очень мало, но дело потихонечку движется. В данной заметке я покажу, как на этом самом Laravel 5 реализовать систему регистрации через инвайты (сиречь приглашения). То есть, когда с инвайтом регистрироваться можно, а без инвайта — нельзя.

Перед разработкой я внимательно прочитал статью Creating an advanced Invitation System in Laravel 4. В ней описано почти то же самое для Laravel 4, так что рекомендую ознакомиться. Главные отличия этой заметки:

  • здесь используется Laravel 5;
  • для ограничения доступа к форме регистрации я вместо фильтра применил middleware (он отлично для этого подходит);
  • для валидации email-ов, вместо разработки отдельного класса-валидатора, используется трейт ValidateRequests.

Что необходимо для реализации инвайтов:

  1. Регистрация — чтобы сделать регистрацию через инвайты, нужно чтобы была принципиальная возможность зарегистрироваться. В стандартной инсталляции Laravel 5 эта возможность уже есть, для наших целей ее достаточно.
  2. Модель для работы с инвайтами и таблица для их хранения.
  3. Форма для создания инвайтов пользователями.
  4. Возможность отправить инвайт на указанный почтовый адрес.
  5. Процедура проверки инвайта для допуска к форме регистрации.
  6. Сохранение инвайта как отработанного при регистрации (чтобы избежать повторного использования).

Начнем с таблицы для хранения инвайтов. Создадим новую миграцию в 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().
Теперь создадим модель для работы с инвайтами (php artisan make:model Invite --no-migration), файл app/Invite.php:
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');

Вот и все, теперь наши инвайты должны работать.

Комментарии