Добрый день! Учусь ларавель, делаем тестовый проект типа облачного хостинга файлов, который возможно станет реальным.
На данный момент все в стадии развернули ларавель, система авторизации (ларавельная) с активацией и сменой мейла (своими), также загрузка и вывод аватара (свои на основе Mediable). Далее в планах подключения биллинга и работа с файлами пользователей.
Все, что сейчас сделано работает. Но хотелось бы советов, по оптимальному проектированию классов.
Для начала примерно показываю что сделал.
Для авторизации и смены мейла написал похожие друг на друга сервисы. И сделал для них интерфейсы и зарегил сервис провайдер. Они включают в себя методы для обработки как запроса на отсылку письма с подтвержением регистрации/нового мейла, так и обработку клика по ссылке из данных писем.
Токены подтвержения регистрации и смены мейла и сам новый мейл, храню в двух отдельных табличках, не в users. И для каждой этой табличке по модели элоквент и по репозиторию.
Привожу код для случая смены мейла:
Роуты
Route::get('home/account/email', ['middleware' => ['auth', 'isVerified'], 'uses' => 'Auth\ChangeEmailController@showForm'])->name('home.account.email');
Route::post('home/account/email_save', ['middleware' => ['auth', 'isVerified'], 'uses' => 'Auth\ChangeEmailController@saveForm'])->name('home.account.email_save');
Route::get('home/account/email_set/{token}', ['middleware' => ['auth', 'isVerified'], 'uses' => 'Auth\ChangeEmailController@emailSet'])->name('home.account.email_set');
Методы контроллера запроса на смену и смены по ссылке из письма
public function saveForm(Request $request, ChangeEmailContract $changeEmailService)
{
$user = Auth::user();
$rules = [
'email' => 'required|email|unique:users',
'password' => 'required|checkpassword:'.$user->email,
];
$messages = [
'email.required' => 'Please enter an email address',
'email.email' => 'Please enter a valid email address',
'email.unique' => 'This e-mail is already taken. ',
'password.required' => 'Please enter your password',
'password.checkpassword' => 'Your enter wrong password',
];
Validator::make($request::all(), $rules, $messages)->validate();
$changeEmailService->sendChangeEmailMail($user, Request::get('email'));
return redirect()->route('home')->with('status', "Confirmation change E-mail link send to ".Request::get('email'));
}
public function emailSet($token, ChangeEmailContract $changeEmailService)
{
$email = Request::get('email');
try {
$user = $changeEmailService->setEmail($token, $email);
}
catch (\App\Exceptions\ChangeEmailNotFoundException $e) {
return redirect()->route('home')
->with('status', $e->getMessage());
}
Auth::login($user);
return redirect()->route('home')
->with('status', 'You successfully activated your new email!');
}
Сервис
namespace App\Services\Auth;
use \App\Models\User;
use \App\Contracts\Auth\ChangeEmailContract;
use \App\Contracts\Auth\ChangeEmailRepositoryContract;
use \App\Exceptions\ChangeEmailNotFoundException;
use Illuminate\Mail\Mailer;
use Illuminate\Mail\Message;
class ChangeEmailService implements ChangeEmailContract
{
protected $mailer;
protected $changeEmailRepo;
public function __construct(ChangeEmailRepositoryContract $changeEmailRepo)
{
$this->changeEmailRepo = $changeEmailRepo;
}
public function sendChangeEmailMail($user, $email)
{
$token = $this->changeEmailRepo->createEmailChange($user, $email);
\Mail::to($email)->send(
new \App\Mail\ChangeEmail(array(
'email' => $email,
'token' => $token,
))
);
}
public function setEmail($token, $email)
{
$changeEmail = $this->changeEmailRepo->getChangeEmailByTokenAndEmail($token, $email);
if ($changeEmail === null) {
throw new ChangeEmailNotFoundException();
}
$user = User::find($changeEmail->user_id);
if (!$user) {
throw new ChangeEmailNotFoundException();
}
$user->email = $email;
$user->save();
$this->changeEmailRepo->deleteChangeEmail($token);
return $user;
}
}
Репо вокруг таблицы где токены и новые мейлы. Понимаю что модель нужно было включить иньекцией, но я предпочел просто обращаться к ней через ORM методы.
namespace App\Repositories\Auth;
use \App\Contracts\Auth\ChangeEmailRepositoryContract;
use \App\Models\EmailChange;
use Carbon\Carbon;
use Illuminate\Database\Connection;
use Illuminate\Support\Facades\DB;
class ChangeEmailRepository implements ChangeEmailRepositoryContract
{
public function createEmailChange($user, $email)
{
$email_change = $this->getEmailChange($user);
if (!$email_change) {
return $this->createEmailChangeRecord($user, $email);
}
return $this->updateEmailChangeRecord($user, $email);
}
public function getEmailChange($user)
{
return EmailChange::where('user_id', $user->id)->first();
}
public function getChangeEmailByTokenAndEmail($token, $email)
{
return EmailChange::where(array('token' => $token, 'email' => $email))->first();
}
public function deleteChangeEmail($token)
{
EmailChange::where('token', $token)->delete();
}
private function updateEmailChangeRecord($user, $email)
{
$token = $this->getToken();
EmailChange::where('user_id', $user->id)->update([
'token' => $token,
'email' => $email,
'created_at' => new Carbon()
]);
return $token;
}
private function getToken()
{
return hash_hmac('sha256', str_random(40), config('app.key'));
}
private function createEmailChangeRecord($user, $email)
{
$token = $this->getToken();
EmailChange::insert([
'user_id' => $user->id,
'token' => $token,
'email' => $email,
'created_at' => new Carbon()
]);
return $token;
}
}
Модель таблицы
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class EmailChange extends Model
{
protected $table = 'email_change';
public function user()
{
return $this->belongsTo(User::class);
}
}
Моделька юзера. Метод отправки письма, связан не с данным, функционалом, а со сменой пароля, которая встроенная используется.
namespace App\Models;
use Illuminate\Notifications\Notifiable;
use App\Notifications\CustomResetPassword;
use Illuminate\Foundation\Auth\User as Authenticatable;
use \Plunk\Mediable;
class User extends Authenticatable
{
use Notifiable;
use \Plank\Mediable\Mediable;
protected $fillable = [
'name', 'email', 'password',
];
protected $hidden = [
'password', 'remember_token',
];
public function sendPasswordResetNotification($token)
{
$this->notify(new CustomResetPassword($token));
}
}
Сервис провайдеры для сервиса и репозитория
namespace App\Providers\Auth;
use Illuminate\Support\ServiceProvider;
class ChangeEmailProvider extends ServiceProvider
{
protected $defer = false;
public function register()
{
$this->app->bind('App\Contracts\Auth\ChangeEmailContract', function ($app) {
return new \App\Services\Auth\ChangeEmailService(
$app -> make("\App\Contracts\Auth\ChangeEmailRepositoryContract")
);
});
}
public function provides()
{
return ['\App\Contracts\Auth\ChangeEmailContract'];
}
public function boot()
{
}
}
namespace App\Providers\Auth;
use Illuminate\Support\ServiceProvider;
class ChangeEmailRepositoryProvider extends ServiceProvider
{
protected $defer = false;
public function register()
{
$this->app->bind('\App\Contracts\Auth\ChangeEmailRepositoryContract', function ($app) {
return new \App\Repositories\Auth\ChangeEmailRepository();
});
}
public function provides()
{
return ['\App\Contracts\Auth\ChangeEmailRepositoryContract'];
}
public function boot()
{
}
}
Как я хочу все это отрефакторить?
Я думаю нужно создать UserRepository, его явно не хватает. В него конечно нужно будет добавить саму регистрацию, смену аватара, а из данного функционала, пожалуй те небольшие процедуры которые относятся все же именно к таблице users например постановка, статус активирован, установка нового мейла...
Тогда получится для каждой таблицы свой репозиторий. В общем то логично. Но кажется более практично было бы, так как активация и смена мейла, в общем то также тесно связанные именно с юзеров вещи и все обращения к таблицам ChangeEmail и Activation а так же генерацию и проверку токена перенести в UserRepository. Да он будет толще зато смогу убрать два репозитория ChangeEmailRepository и ActivateEmailRepository и также два контракта и два сервис провайдера..
Тогда получится юзеру и связанным тесно с ними табличкам общий репозиторий, а вот сервисы отдельные для смены емайла, активации и аватара оставить.
Как считаете, так было бы оптимальнее?
Так же прошу подсказать есть в моем уже существующем и приведенном коде какие то явные ошибки?
И еще вопрос. А даже если ChangeEmailRepository и ActivateEmailRepository оставить отдельными и не переносить их содержимое в UserRepository, то нужны ли для этих репозиториев контракты и сервис провайдеры, учитывая что подменять реализацию я ведь вряд ли буду....