Русское сообщество разработки на PHP-фреймворке Laravel.
Ты не вошёл. Вход тут.
Я ими не пользуюсь, т.к. гораздо удобнее элементы формы (например) грамотно назвать, чтобы не нужно было преоразование данных делать.
Тогда понятно.. Нет меня интересует не столько названия полей итд. Сколько создание объекта с еще какими то дополнительными действиями перед ним. Скажем создали объект через API на удаленном сервисе/платежном шлюзе, а затем в свою БД сохранили. И как бы все это что бы $user->some_child_objects()->createMethod($request->all()); ??
Это же типичная такая задача... Нужно что бы внутри createMethod был доступен $user родительский и данные с которыми создаем ну например из формы пришедшие... Или тут только сервис?
Так часто называют классы, которые переводят данные из одного формата в другой.
А можешь привести тут или написать в своем репозитории пример преобразователя конкретный и именно при создании модели и/или при создании дочерней модели через отношения?
А если я хочу закрыть одну форму? Но ведь если закрывать в реквестах то мне вместо одного тогда придется создавать 2 request'а, в show form и save form. Это же не красиво..
Алексей, а middleware закрыть скажем все форму к которой в некоторой ситуации нет доступа не верно? Ну скажем и показ формы и отправку закрыть прямо в конструкторе контроллера вызвав middleware ? Скажем если у юзера только одна подписка может быть, а она уже есть. То закрыть например даже админу форму добавки подписки для юзера с этим ID с помощью middleware ?
Где это лучше проверять? Как-то пытаться добавить в SomeEditRequest?
Вроде бы в Request есть функция authorize() которая хорошее место что бы проверить то о чем говорите.
Кроме того проверить имеет ли вообще юзер доступ к данной форме можно проверить и в middleware. Тем более что им можно закрывать не один request скажем именно отправки данной формы, а удобно закрывать и несколько роутов или методов контроллера.
Поточнее когда лучше проверять права в Request а когда в Middleware адресую вопросы более опытным участникам форума!
Допустим нужно создать объект в простом случае это просто Subscription::create($arr); или допустим дочерний через отношение $users->subscriptions()->create($arr);
Ну, а что если случай посложнее и при создании объекта еще логика выполняется, как то данные подготавливаются.
Например, через сервис. Это мой код, но я структуру в laravel cashier пакете подглядел
Класс и трейт Пользователя (копирую не полностью там еще много всего)
class User extends Authenticatable
{
use Billable;
trait Billable
{
/**
*
* @return Collection
*/
public function subscriptions()
{
return $this->hasMany(Subscription::class, $this->getForeignKey())->orderBy('created_at', 'desc');
}
/**
* Creating a new subscription.
*
* @param string $subscription
* @param Plan $plan
* @return SubscriptionBuilder
*/
public function newSubscription($plan)
{
return new SubscriptionBuilder($this, $plan);
}
Класс SubscriptionBuilder наверное не четко сервис, но скорее можно воспринимать как сервис, тк он включает в себя модель и используется для создание дочерней модели Subscription в зависимости от параметров, кроме того включает в себя функционал создания подписки в платежном шлюзе. Те можно сказать реализует фичу взять юзера, создать ему клиента, карту и подписку через API в платежном шлюзе и затем создать экземпляр дочерней модели Subscription.
Копирую полностью
<?php
namespace App\Classes\Models;
use App\Classes\Services\BraintreeService as GatewayService;
use Exception;
use Carbon\Carbon;
use Braintree_Subscription as GatewaySubscription;
use App\Classes\Models\Plan;
use Illuminate\Support\Facades\Config;
class SubscriptionBuilder
{
/**
* Owner user
*
* @var User
*/
protected $user;
/**
*
* @var Plan
*/
protected $plan;
/**
*
* @var bool
*/
protected $addTrial = false;
/**
*
* @var int
*/
protected $trialDurationUnit;
/**
*
* @var string
*/
protected $trialDuration;
/**
* Create a new subscription builder instance.
*
* @param mixed $user
* @param string $name
* @param Plan $plan
* @return void
*/
public function __construct($user, $plan)
{
$this->plan = $plan;
$this->user = $user;
}
/**
*
*
* @param int $trialDuration
* @param int $trialDurationUnit
* @return $this
*/
public function addTrial($trialDuration, $trialDurationUnit)
{
$this->trialDuration = $trialDuration;
$this->trialDurationUnit = $trialDurationUnit;
$this->addTrial = true;
return $this;
}
/**
* Create a new subscription.
*
* @param string|null $token
* @param array $customerOptions
* @param array $subscriptionOptions
* @return Subscription
* @throws \Exception
*/
public function create($token = null)
{
if ($token) {
if ($this->user->gateway_id) {
throw new \LogicException('When user have payment method add subscription only with this method');
} else {
$this->user->createGatewayCustomerWithPaymentMethod($token);
}
}
if (!$this -> addTrial) {
$response = GatewaySubscription::create([
'planId' => $this->plan->gateway_id,
'price' => (string) round($this->plan->cost, 2),
'paymentMethodToken' => $this->user->defaultCard->gateway_id,
'trialPeriod' => false,
]);
} else {
$response = GatewaySubscription::create([
'planId' => $this->plan->gateway_id,
'price' => (string) round($this->plan->cost, 2),
'paymentMethodToken' => $this->user->defaultCard->gateway_id,
'trialPeriod' => true,
'trialDurationUnit' => $this-> trialDurationUnit,
'trialDuration' => $this-> trialDuration,
]);
}
if (! $response->success) {
throw new Exception('Gateway failed to create subscription: '.$response->message);
}
if ($this->addTrial) {
if ($this->trialDurationUnit == Subscription::TrialDurationUnitDay) {
$trialEndsAt = Carbon::today()->addDays($this->trialDuration);
} else if ($this->trialDurationUnit == Subscription::TrialDurationUnitMonth) {
$trialEndsAt = Carbon::today()->addMonth($this->trialDuration);
} else {
throw new \LogicException('Need trial duration unit.');
}
}
return $this->user->subscriptions()->create([
'name' => \Config::get('services.subscription.name'),
'gateway_id' => $response->subscription->id,
'plan_id' => $this->plan->id,
'trial_ends_at' => $this -> addTrial ? $trialEndsAt : null,
'card_id' => $this->user->defaultCard->id,
]);
}
}
В контроллере вызывается так:
Хотя могут быть разные понятно контексты и аргументы вызова.
$user->newSubscription($plan)->addTrial(Config::get('services.subscription.trial_duration'), Config::get('services.subscription.trial_duration_unit'))->create($request->payment_method_nonce);
Собственно все работает и даже особенно дублирования кода я не вижу.
Но теме не менее хочется на этом примере разобраться какие еще паттерны тут можно применить и что можно зарефакторить.
Например, возникает мысль, а можно ли отдать логику создания подписки в шлюзе и одновременно самой модели, собственно классу Subscription ? Причем так что бы вызывать этот метод создания через отношения
Например
$user->subscriptions->createWithGateway($plan, $trialPeriod, $trialDuration);
Внутри метода будет наверное что то вроде того
class Subscription
{
function createWithGateway($plan, $trialPeriod, $trialDuration)
if (!$trialDuration || !$trialDurationUnit) {
$response = GatewaySubscription::create([
'planId' => $plan->gateway_id,
'price' => (string) round($plan->cost, 2),
'paymentMethodToken' => $this->user->defaultCard->gateway_id,
'trialPeriod' => false,
]);
} else {
$response = GatewaySubscription::create([
'planId' => $plan->gateway_id,
'price' => (string) round($plan->cost, 2),
'paymentMethodToken' => $this->user->defaultCard->gateway_id,
'trialPeriod' => true,
'trialDurationUnit' => $trialDurationUnit,
'trialDuration' => $trialDuration,
]);
}
if (! $response->success) {
throw new Exception('Gateway failed to create subscription: '.$response->message);
}
if ($this->addTrial) {
if ($trialDurationUnit == Subscription::TrialDurationUnitDay) {
$trialEndsAt = Carbon::today()->addDays($this->trialDuration);
} else if ($trialDurationUnit == Subscription::TrialDurationUnitMonth) {
$trialEndsAt = Carbon::today()->addMonth($this->trialDuration);
} else {
throw new \LogicException('Need trial duration unit.');
}
}
return self::create([
'name' => \Config::get('services.subscription.name'),
'gateway_id' => $response->subscription->id,
'plan_id' => $plan->id,
'trial_ends_at' => $trialEndsAt ? $trialEndsAt : null,
'card_id' => $this->user->defaultCard->id,
]);
}
}
Интересно какие еще есть хорошие практики создания объекта со сложными условиями и дополнительными действиями? Возможно преобразователи?
1) А если например, сервис должен отправить мейл с кодом активации это из одного контроллера при регистрации, а затем проверить, что этот код введенный в ссылке из письма верен это конечно в другом контроллера. И общее во всем этом это таблица с кодами активации это вообще один сервис или два, или вообще лучше без сервиса и просто дергать в контроллерах этих нужные модельки?
2) Подписку можно отменить из ЛК юзера и из Админки, логика удаления очень похожа. Ну просто типа $user->subscription->cancel() хотя ну мало ли могут быть нюансы разные для юзера и админа. Скажем еще что то перед этим проверить или после этого обновить. Это сервис желателен или достаточно опять же в двух контроллерах просто делать что нужно и дергать $user->subscription->cancel() ?
Как четко понять что нужен именно сервис?
Создай папку Traits в каталоге app, и туда ложи все трейты
У меня так и есть. Просто не удобно каждый раз туда лазить за трейтом. Проще вместе с моделями. Подряд открываешь и их и трейты
А хорошо ли положить трейты прямо в ту папку где модели? Типа BillableTrait.php
Нет, но наверняка есть много инструментов вроде этого.
Добавил в закладки как разберемся с докером попробую.
Кстати а знаешь ли редактор или IDE у которого php код автоматом форматируется корректно?
Хорошо понятно. После и перед фигурными скобками строку не пропускаем
Пропуски строк между блоками кода
Как из вариантов вернее?
1.
public function resume(Request $request)
{
$user = Auth::user();
try {
$user->subscription->resume();
} catch (\Throwable $e) {
return redirect()->route('home.subscription')->with('status', $e->getMessage());
}
return redirect()->route('home.subscription')->with('status', 'Subscription resume');
}
2.
public function resume(Request $request)
{
$user = Auth::user();
try {
$user->subscription->resume();
} catch (\Throwable $e) {
return redirect()->route('home.subscription')->with('status', $e->getMessage());
}
return redirect()->route('home.subscription')->with('status', 'Subscription resume');
}
3.
public function resume(Request $request)
{
$user = Auth::user();
try {
$user->subscription->resume();
} catch (\Throwable $e) {
return redirect()->route('home.subscription')->with('status', $e->getMessage());
}
return redirect()->route('home.subscription')->with('status', 'Subscription resume');
}
Кажется такие вещи на коленке не делаются. Первое что приходит на ум cdn нужен на котором файлы хранить на другом сервере чем морда сайта. И его дергать по API с морды на которой ларавель. А на морде регистрация, авторизация, подписка на платные услуги и личный кабинет в котором хранятся купленные картинки
В общем как правильно сделать? И что бы меньше писать..
В этом случае какой-нибудь phpDocumentor
Мы им не пользуемся..
что такое dates и что такое guarded.
Это есть в документации в отличии например от содержимого собственных методов
Правильно ли я понимаю, что в 7-ке достаточно писать так:
try
{
// Code that may throw an Exception or Error.
}
catch (Throwable $t)
{
// Executed only in PHP 7, will not match in PHP 5
}
И тогда поймаются и php ошибки и исключения которые скажем в коде пакета выбрасываются как throw new SomeException() ??
try {...} catch (\Exception $e) {
Это очевидно, я же так и написал сейчас...
Да только сейчас читаю..
try {...} catch (\Exception $e) {
Это очевидно, я же так и написал сейчас...
Везде в том числе в исходниках ларавель, вижу и пишу в своем коде комменты типа:
/**
* The attributes that aren't mass assignable.
*
* @var array
*/
protected $guarded = [];
/**
* Indicates plan changes should be prorated.
*
* @var bool
*/
protected $prorate = true;
/**
* Get the user that owns the subscription.
*/
public function user()
{
return $this->owner();
}
Но мне кажется писать их у каждого очевидного метода и поля класса лишнее, тормозит разработку и засоряет файлы. Итак ясно что такое dates что такое guarded, что за методы с отношениями, валидациями итд итп, тем более все это повторяется в множестве классов. А может и не писать? Как тут принято? Можно ли писать только у существенных и собственных методов..
Хотя бы тип исключения показать... И строку и стек вызовов. В дальнейшем можно сделать условие что бы это показывалось только на тестовом сервере, а на боевом писалось в лог.
Да и у меня также значит оставлю..
try {
$user->newSubscription(Config::get('services.subscription.name'), $plan->braintree_plan)
->create($request->payment_method_nonce, [
'email' => $user->email,
]);
} catch (\Exception $e) {
return redirect()->route('home.upgrade')->with('status', $e->getMessage());
}
Внутри try происходит исключение. Но в $e->getMessage() оно в каком то урезанном не информативном виде. только содержимое поле #msg а как более подробно вывести?
А имеет ли смысл класть все контроллеры/вьюхи которые относятся к ЛК пользователя и к админке в отдельные папки home admin я так сделал, но теперь кажется что толку то нет..