Laravel по-русски
Русское сообщество разработки на PHP-фреймворке Laravel.
Ты не вошёл. Вход тут.
Если я все таки уверен, что кроме элоквент моделей и контроллеров, нужен еще слой. То скажи пожалуйста лучше, что бы это было
-Репо
-Сервисный слой
— И то и другое по надобности.
Для крупнейших таблиц и основной функционала UserRepo UserFilesRepo UserSubscriptionsRepo
Или вообще для каждого функционала и почти каждой таблицы UserRepo UserChangeEmailRepo UserActivationRepo итд? Мне кажется скорее для крупнейших…
А методы работы с будующим функционалом связанным с юзером, например файлы юзера (не авы), подписки и платежи юзера? Это что тоже в eloquent моделях? Каких Models\User неверно кажется да и гигантская будет. В Models\User и Models\UserFiles например? Тоже кажется не очень тк совмещаем модель как отражение таблицы конкретной которых может быть несколько для функционала сайта и модель как место хранения бизнес логики — юзеров, подписок юзеров, файлов юзеров итд.
Разбрасывать элоквент запросы по контроллерам и вью композерам тоже не хочется… Ну пусть не доктрине, пусть ее не будет. Но все таки слой где все что связанно с запросами весьма удобен в большом приложении…
- Repo не нужен
Где ты предлагаешь бизнес логику для юзера писать прямо в модельке элокьент? В классе Models\User?
Мне кажется это неверно… Это же класс именно Eloquent зачем я буду писать в него скажем метод отдающий на выходе скажем коллекцию юзеров… Это же совершенно не логично. Класс описывает одного юзера причем ради использования его в контексте запросов элокьент, а я возвращаю коллекцию…
Или ты об том, что бы вообще никуда не собирать бизнес-логику и запросы для юзера. А прямо в контроллере писать Eloquent запросы? Но не хочется это по контроллерам разбрасывать…
...
Здесь нужно следить, чтобы методы hasMedia и getMedia не наделали дополнительных запросов в БД, особенно если используются сторонние пакеты.
Проверю. Скорее дают.. Но как иначе я узнаю показать ли аву.. Впрочем это сейчас не главные. Все равно это запросы по ключу выбирающие одну строку. И десятков миллионов юзеров пока не придвидется
Выше писал. Ответишь?Ты вопросы клепаешь быстрее, чем я ответы пишу.
я же волнуюсь скоро отпуск через несколько дней, а я хочу оставить годную основу для сайта
Итого сервис не нужен, репо нужен.
В репо убираем все запросы к БД. и дергаем репо из контроллера. Согласен, что в случае смены мейла, в сервисе мало чего для повторного пользования…
Но в случае если все таки одни и теже запросы к репо с одной и той же логикой используется в нескольких контроллерах, то тогда нужно вынести в сервиc??
Как я понял, тебе список нужен на одной или двух страницах, тогда почему бы не передать список в контроллере:return view('my_view', [
'topUsers' => $this->user->getTopUsers(),
'friends' => $this->user->getTopFriends()
]);Это простые задачи, которые незачем усложнять.
Да, я все это понимаю. Но нет с картинками чуть сложнее и пакет нам понравился. Впрочем сейчас актуальнее самая основа про активацию и смену мейла, про UserRepositiry и сервисы и как все это верно биндить. В общем самая основа проекта.
Выше писал. Ответишь?
Насчет аватара. Код его получения примерно такой:
$user = Auth::user();
if ($user) {
if($user->hasMedia(’avatar’)) {
$user_avatar_url = $user->getMedia(’avatar’)->first()->getUrl();
}
}
Тк я использую пакет mediable. Хранить просто как user_id.jpg не хочу. Тк с аватаром может пока и все просто, но вообще нужно хранить оригиналы несколько версий ресайза в разный размер, что бы юзер вырезал показываемую область, галлерии изображений.
В общем в перспективе то нам нужно проект enterpise уровня на laravel сделать. Тк есть https://www.realmusic.ru/ с ужасным кодом старым, нужно или его переписать или заново что то на основе тех наработок… В общем нужно заложить фундамент по универсальней…
« тогда читай документацию и книги/статьи о хороших практиках при разработке.»
Я читал все что нашел релевантное своей задачи. И вопрос мой тоже из-за этого. Так как есть работает. И если просто взять и юзать лишь элоквент из контроллеров тоже будет работать…
Я хочу нечто среднее и с прицелом под задачи о которых выше пишу.
«Репозитории хороши когда ты используешь сырые запросы или обращения к стороннему API»
Сырые запросы будут. Сайт не простой, одним элоквент не обойтись скорее… Возможно и обращения к стороннему API будут например к биллингу или к API сервера где файлы хранятся.
Кроме того мы хотим использовать лару на старом и довольно сложном самописном сайте сначала сделав на ней админку к нему. Там уже существующие таблицы забитые милионами записей… И миграции с нуля не начнешь накатывать…
С учетом сказанного использование Repo хотя бы только и именно для User и возможно проба внедрить доктрину пусть не в этом, так в админке, оправданно??
Понимаю, что все зависит от задачи… Но вот например вывод аватара… допустим он нужен наверху авторизованному юзеру в хидере, нужен где нибудь на отдельной странице где скажем список юзеров онлайн, список друзей юзера, или тому подобное, те список юзеров и нужен на странице его загрузки и обновления возможно более крупная версия…
Брать ссылку на аватар, которую сформировали в вью-композере для хидера, хранить ее внутри объекта-сервиса, что бы вывести где то еще скорее не нужно. Потому синглтон не нужен.
Но и повторять код формирования ссылки на аватар в вью-композере для хидере, затем в контроллере где текущий аватар для обновления или где список юзеров также не нужно.
Значит выход делаем просто bind класса-сервиса. И обращаемся в вью-композере и указанных контроллерах, либо чем экземпляр класса полученный иньекцией зависимости или через фасад…
Таким образом и код не повторяется и лишних сущностей или хранения каких то значений не нужных в контексте всего приложения нет.
Кстати если я оставляю все таки сервисы. То видимо лучше как и сейчас регистрировать их каждый в своем сервис провайдере? тк каждый из них используется лишь на одной двух страницах сайта… И нужно включить отложенную загрузку… Хотя честно не понимаю какой прирост в скорости от того что простенький класс не создастся… А если сервис используется много где, то лучше в AppService для всего сайта?
А репозиторий UserRepository видимо уж точно лучше биндить в AppService? Он то для всего нужен…
И все это лучше именно bind, а не singleton как понимаю… Тк ничего такого что нужно было бы прохождении запроса сохранять между вызовами методов класса, в его экземпляре нет. А значит нет надобности что бы этот экземпляр был единственным…
Спасибо за мысль. Я тоже считаю что лишнего много.
В тоже время хочется и правильности и универсальности поэтому до предела упрощать не хочется.
- 2-3 однострочных метода в модели,
Прямо в классах Eloquent не хочется. Кажется репо все же нужно. туда же можно для работы с юзером и много другое пихнуть. Кроме того например коллекцию из нескольких юзеров лучше в репо получать. Это более логично чем в модельке eloquent. Ну и потом а если на Doctrine переметнуться? А такая мысль есть…
В общем на eloqent не хочется затачиваться… И Repo будет.
А вот от контракта и от провайдера, для репо можно без потерь отказаться? И также имеет ли смысл в Repo для юзера запихнуть его авторизацию и смену мыла? Вот эти вопросы кажется ключевые. Упростив именно все это кажется будет уже и достаточно…
Про отказ от сервисов и перенос в контроллер, тут + на —, думаю можно и так и сяк…
Остальные замечания принимаю. Языковых файлов пока не надо. Сайт только на english
Добрый день! Учусь ларавель, делаем тестовый проект типа облачного хостинга файлов, который возможно станет реальным.
На данный момент все в стадии развернули ларавель, система авторизации (ларавельная) с активацией и сменой мейла (своими), также загрузка и вывод аватара (свои на основе 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, то нужны ли для этих репо контракты и сервис провайдеры, учитывая что подменять реализацию я ведь вряд ли буду…
Проблема с правами на сервере оказалось. А по началу не в папке проекта к composer обращался. Админ смог установить нормально пакет
Всем привет!
Решил разобраться с установкой пакетов добавляя в composer.json
Вот его содержимое
{
"name": "laravel/laravel",
"description": "The Laravel Framework.",
"keywords": ["framework", "laravel"],
"license": "MIT",
"type": "project",
"require": {
"php": ">=5.5.9",
"laravel/framework": "5.2.*",
"landish/pagination": "~1.0",
"laravelcollective/html": "5.1.*"
},
"require-dev": {
"fzaninotto/faker": "~1.4",
"mockery/mockery": "0.9.*",
"phpunit/phpunit": "~4.0",
"symfony/css-selector": "2.8.*|3.0.*",
"symfony/dom-crawler": "2.8.*|3.0.*",
"illuminate/html": "~5.0",
"zizaco/entrust": "dev-laravel-5"
},
"autoload": {
"classmap": [
"database"
],
"psr-4": {
"App\\": "app/"
}
},
"autoload-dev": {
"classmap": [
"tests/TestCase.php"
]
},
"scripts": {
"post-root-package-install": [
"php -r \"copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"php artisan key:generate"
],
"post-install-cmd": [
"php artisan clear-compiled",
"php artisan optimize"
],
"post-update-cmd": [
"php artisan clear-compiled",
"php artisan optimize"
]
},
"config": {
"preferred-install": "dist"
}
}Добавлено:
"illuminate/html": "~5.0",
"zizaco/entrust": "dev-laravel-5"
Пробовал и один пакет добавлять и не в require-dev а в require
Запускаю composer update
Результат:
Loading composer repositories with package information
Updating dependencies (including require-dev)
Nothing to install or update
Generating autoload files
То есть ничего не добавляется. В папке vendor смотрю ничего нового. Пробую создать service provider связанный с установленным пакетом класс не найден..
[size="1"][color="grey"]Добавлено через 25 минут[/color][/size]
alexey@nf-web1:~$ composer require zizaco/entrust
Using version ^1.7 for zizaco/entrust
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
- Installing doctrine/inflector (v1.1.0)
Loading from cache
- Installing illuminate/contracts (v5.2.31)
Downloading: 100%
- Installing paragonie/random_compat (v1.4.1)
Downloading: 100%
- Installing illuminate/support (v5.2.31)
Downloading: 100%
- Installing illuminate/cache (v5.2.31)
Downloading: 100%
- Installing symfony/console (v3.0.4)
Downloading: 100%
- Installing illuminate/console (v5.2.31)
Downloading: 100%
- Installing zizaco/entrust (1.7.0)
Downloading: 100%
paragonie/random_compat suggests installing ext-libsodium (Provides a modern crypto API that can be used to generate random bytes.)
illuminate/support suggests installing illuminate/filesystem (Required to use the composer class (5.2.*).)
illuminate/support suggests installing jeremeamia/superclosure (Required to be able to serialize closures (~2.2).)
illuminate/support suggests installing symfony/polyfill-php56 (Required to use the hash_equals function on PHP 5.5 (~1.0).)
illuminate/support suggests installing symfony/process (Required to use the composer class (2.8.*|3.0.*).)
illuminate/support suggests installing symfony/var-dumper (Improves the dd function (2.8.*|3.0.*).)
illuminate/cache suggests installing illuminate/database (Required to use the database cache driver (5.2.*).)
illuminate/cache suggests installing illuminate/filesystem (Required to use the file cache driver (5.2.*).)
illuminate/cache suggests installing illuminate/redis (Required to use the redis cache driver (5.2.*).)
symfony/console suggests installing symfony/event-dispatcher ()
symfony/console suggests installing symfony/process ()
symfony/console suggests installing psr/log (For using the console logger)
illuminate/console suggests installing guzzlehttp/guzzle (Required to use the ping methods on schedules (~5.3|~6.0).)
illuminate/console suggests installing mtdowling/cron-expression (Required to use scheduling component (~1.0).)
illuminate/console suggests installing symfony/process (Required to use scheduling component (2.8.*|3.0.*).)
Writing lock file
Generating autoload filesВот таким образом пакеты скачиваються устанавливаются, а в папке vendor ничего нового нет, поиском zizaco искал текст/папки по всему проекту и ничего не нашел.