Студия разработки сайтов и приложений

Netspark.ru

Умный ежедневник турагента

Доктурист

Платформа для ботов в Telegram

Ботопотамы

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

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

lrvinvites1

Перед разработкой я внимательно прочитал статью 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
Это обычная форма, в которой есть поле для ввода почты, многострочное поле для ввода сопроводительного текста и кнопка отправки. Ну и немного валидации, конечно.
lrvinvites2

Теперь создадим контроллер, показывающий и сохраняющий эту форму (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');

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

Обсуждение

Чтобы обсудить заметку, написать комментарий, или просто связаться, заходите в Телеграм-канал. У нас весело и всем рады!

Также меня можно найти в Хвиттере, VC.ru, Дзене, или Тенчате. А если вы на парковке, присоединяйтесь к каналу в Max!