Русское сообщество разработки на PHP-фреймворке Laravel.
Ты не вошёл. Вход тут.
В каком случае лучше применять middlewate или метод authorize в request классе?
Мой кейс сейчас таков. Пишу скрипт файлового хостинга с Dropbox подобным функционалом. Нужно в различных случаях проверять права на папки по пути к ним.
К пример http://site.com/files/test/test2/test3/test4 верен ли путь до папки test4? Совпадает ли со структурой в БД? Принадлежит ли все это авторизованному юзеру? Но также нужно проверять другие роуту, по одному из них папка добавляется, по другому перемещается или удаляется. Иногда нужно проверить один путь, а в других случаях скажем перемещения папки проверить нужно два пути, исходный и конечный.
Для всего этого я создал в классе User метод который проходит циклом по пути и делает запросы нужные. На выходе получается найденный объект Folder который нужен в дальнейшем или false если путь не верный или юзеру не принадлежит. Можно конечно в сервис это запихнуть..
Но суть в том где этот метод вызывать в middleware или в request'ах в методе authorize.
Привежу код который работает в middleware и дальнейшее использование в контроллере
public function handle($request, Closure $next)
{
if (!$request->user()->buildCurrentFolderPath($request->route('path_parts'))) {
return redirect()->route('files')->with('status', 'wrong link');
}
return $next($request);
}
public function show(Request $request, $path_parts = null)
{
$user = $request->user();
$folders = $user->current_folder->getChildFolders();
return view('files.files', [
'folders' => $folders,
'path_parts' => $path_parts,
'user' => $user,
]);
}
И на всякий модель
public function buildCurrentFolderPath($path = null)
{
$pathArr = explode('/', $path);
$currentFolderPath = [];
$currentFolder = $this->rootFolder;
$currentFolderPath[] = $currentFolder;
foreach ($pathArr as $path_part) {
if (!$path_part) continue;
$currentFolder = $this->folders()
->where("name", $path_part)
-> where('parent_id', '=', $currentFolder->id)
->first();
if (!$currentFolder) {
return false;
}
$currentFolderPath[] = $currentFolder;
$currentFolder->path = $currentFolderPath;
}
$this->current_folder = $currentFolder;
return true;
}
Тут еще в данном методе данные для хлебных крошек собираются, тк при просмотре папки они нужны и что бы два раза в БД не лазить. Но это я постараюсь в другой метод вынести.
Вопрос скорее где правильнее такой метод вызывать?
Насчет использования паттерна nested tree то он нам не подходит решили... Не хотим осложнять вставку и хранить лишнее.
Оплатить именно то есть сделать charge не даст. А вот добавить подписку конечно даст. С точки зрения шлюза юзер имеет право на кучу подписок...
Впрочем мы уже решили их подписками и планами регулярных платежей не пользоваться хоть оно и просто и удобно. А на своей стороне следить за регулярными платежами и делать из крона charge когда подходит время обновить подписку. По большей части нас это не нужно что бы подписка не зависела от одного конкретного шлюза типа braintree.
А у тебя есть опыт подобных разработок?
С БД то и так все понятно... Вот в шлюзе (stripe or braintree)создать случайно две подписки одному юзеру значит они дважды будут снимать регулярные платежи...
Дело в том, что braintree позволяет множество подписок для юзера. При том что каждая снимает регулярные платежи. А вот как мы проектируем наш сайт это один юзер и одна активная подписка одновременно. А если использовать запрос сначала к braintree и затем в случае успеха insert в нашу БД, то реально возникает race condition и может наплодиться подписок в braintree. И проверка в самом начале процедуры записи в БД не помогает...
Ну пока лид сказал блокировать кнопку и игнорить... И потом еще подумаем
имеет ли данный пользователь редактировать эти данные.
Тут скорее политики нужны https://laravel.com/docs/5.5/authorizat … g-policies
Что много раз жать на кнопку что одновременно нажать на нее в разных браузерах суть то одна повторные запросы..
Сессия не причем. А если из другого браузера или админ добавляет подписку одновременно с юзером. Тут нужен именно мутекс. А вот насчёт добавления карты повторного это похоже на защиту от флуда..
А еще оказалось семафор для блокировки прямо встроен в php http://php.net/manual/ru/function.sem-get.php
Нашел такой пример:
$sem = sem_get(1234, 1);
if (sem_acquire($sem)) {
//successful lock, go ahead
sem_release($sem);
} else {
//Something went wrong...
}
Да и мне тоже этот mutex нужно реализовать, но как лучше...
Вижу например вот такое https://github.com/arvenil/ninja-mutex с мемкешем..
Но может и самому написать.. Как я понимаю если скажем User может иметь только одну активную подписку Subscription и нужно избежать race condition во время ее создание. То блокируем user->id mutex'ом на создание новых подписок, в начале процедуры создания подписки и скажем на минуту. И при успешном создании убираем mutex.
А если например User может иметь несколько кредитных карт Cards и повторно добавляет одну и туже карту... Блокировать должны видимо именно на добавление данной карты, те записать в мемкеш ее уникальный токен из формы и не принимать с данным токеном запросы более
Правильно ли я все понял?
Я читал это. Но там не до конца конкретно. Да проверять надо на бекенде... Лочить возможность добавки пока первый запрос не прошел, отсекая повторные. Собственно про то как это лучше сделать я и спрашиваю.
Всем привет!
Продолжаю работать над проектом с оплатой и подписками braintree.
Возник вопрос защиты от повторных запросов на подписку пользователя. Собственно если не блокировать кнопку submit их легко воспроизвести. А проверка в начале обработки запроса на то, что пользователь уже подписан, не помогает. Все потому что создание подписки состоит из двух частей запроса через API к шлюзу и добавление в БД. Запрос к Braintree может и пару секунд занять. И возникает race_conditions.
Насчет того что бы сначала создать подписку в БД в статусе "не связана с Braintree", затем обратиться к Braintree и обновить подписку в БД по результатам. Да так можно и тогда данной проблемы нет.. Но так по ряду причин не хочется.
Вопрос как лучше быть? В начале создания подписки залочить юзера на создание новых подписок через поле в БД в users или создать в Cache запись... Понятно дело что в конце запроса когда подписка уже сохранена в БД можно снять лок. Как то бороться с помощью повторной отправки именно самой формы считаю смысла нет. Ну а если в разных табах для одного и того же юзера подписку добавляем таже ведь проблема..
Код создания подписки
public function createSubscription($request)
{
if (isset($request['payment_method_nonce'])) {
$gatewayCustomer = $this->findOrCreateCustomer();
$this -> createPaymentMethod($request['payment_method_nonce'], $gatewayCustomer);
}
$trial_ends_at = isset($request['trial_ends_at']) ? $request['trial_ends_at'] : null;
$plan = Plan::findOrFail($request["plan_id"]);
if (!empty($this->defaultCard)) {
$gatewaySubscription = $this -> createGatewaySubscription($plan, $trial_ends_at);
}
$subscription = $this->subscriptions()->create([
'plan_id' => $plan->id,
'trial_ends_at' => $trial_ends_at,
'gateway_id' => isset($gatewaySubscription) ? $gatewaySubscription -> id : null,
'card_id' => isset($this->defaultCard) ? $this->defaultCard->id : null,
]);
}
Логика учитывает создание подписки и юзером и админом. И в том числе и с обращением и без к Braintree
Я бы весь request в аргумент метода точно в таком случае выборке по фильтру бы не запихивал..
Вообще какую бы Вы выбрали архитектуру для SAAS приложения, которая не привязано жестко к одному шлюзу, а где User может выбирать скажем оплату и подписку картой через Stripe, а оплату и подписку PayPal чеерез Braintree, а может и еще какие то платежные методы и шлюзы для регулярных платежей...
Понятно можно сделать что то похожее на Laravel cashier с наследованием от интерфейса или абстрактного класса и полимформизмом метода, скажем метод newSubscription() для одного метода подписки так реализован, для другого сяк. Также isSubscriebed() OnTrial() итд. Но вот поточнее бы структуру кто порекомендовал.. И как то может с trait'ами для юзера намутить... Что бы вызывали скажем всегда $user->newSubscription() $user->isSubscriebed() но вот где то дали понять метод какого типа наследника использовать... Может если речь о создании новой именно подписки то что то вроде паттерна фабрика, фабричный метод. А уже для существующей подписки, выбранные метод будет полем модели юзер и в завимости от его значения тот или иной наследник будет подгружаться...
В общем какое то бы простое красивое решение
Понял. И от чего тут можно отнаследовать? сделать интерфейс и от него два сервиса? Или от класса User отнаследовать 2 класса, каждый из который подключает свой trait ?
Скажем при одном запросе в контроллере User был с trait BillableStripe а при другом BillableBraintree. Вопрос не столько о синтаксисе и ООП. А скорее о паттернах и практиках. Или тут скорее создать два сервис провайдера включающих зависимость от User и вызывать в контроллере или тот или другой по надобности смотря какую форму отправили?
В общем в итоге получилось вот что. В зависимости от условий создания Subscription объекта и связанного объекта в шлюзе, создаю все это в двух разных методах трейта класса User.
public function createSubscriptionByAdmin($request)
{
$plan = Plan::findOrFail($request['plan_id']);
if ($this->hasCard()) {
if (empty($request['trial_ends_at'])) {
$response = GatewaySubscription::create([
'planId' => $plan->gateway_id,
'paymentMethodToken' => $this->defaultCard->gateway_id,
'options' => ['startImmediately' => true]
]);
} else {
$response = GatewaySubscription::create([
'planId' => $plan->gateway_id,
'paymentMethodToken' => $this->defaultCard->gateway_id,
'firstBillingDate' => $request['trial_ends_at'],
]);
}
if (!$response->success) {
throw new Exception('Gateway failed to create subscription: '.$response->message);
}
return $this->subscriptions()->create([
'gateway_id' => $response->subscription->id,
'card_id' => $this->defaultCard->id,
'current_period_end' => $response->subscription->billingPeriodEndDate,
'plan_id' => $plan->id,
'trial_ends_at' => $request['trial_ends_at'],
]);
} else {
return $this->subscriptions()->create([
'plan_id' => $plan->id,
'trial_ends_at' => $request['trial_ends_at'],
]);
}
}
public function createSubscription($request)
{
if (isset($request['payment_method_nonce'])) {
if ($this->gateway_id) {
throw new \LogicException('When user have payment method add subscription only with this method');
} else {
$this->createGatewayCustomerWithPaymentMethod($request['payment_method_nonce']);
}
} else {
if (!$this->gateway_id) {
throw new \LogicException('When user have not payment method add subscription only with add payment data');
}
}
$plan = Plan::findOrFail($request['plan_id']);
if (empty($request['trial_ends_at'])) {
$response = GatewaySubscription::create([
'planId' => $plan->gateway_id,
'paymentMethodToken' => $this->defaultCard->gateway_id,
'options' => ['startImmediately' => true]
]);
} else {
$response = GatewaySubscription::create([
'planId' => $plan->gateway_id,
'paymentMethodToken' => $this->defaultCard->gateway_id,
'firstBillingDate' => $request['trial_ends_at'],
]);
}
if (! $response->success) {
throw new Exception('Gateway failed to create subscription: '.$response->message);
}
return $this->subscriptions()->create([
'gateway_id' => $response->subscription->id,
'card_id' => $this->defaultCard->id,
'current_period_end' => $response->subscription->billingPeriodEndDate,
'plan_id' => $plan->id,
'trial_ends_at' => $request['trial_ends_at'],
]);
}
Вызывается в контроллерах так:
Для админа
$user-> createSubscriptionByAdmin();
В Request при этом plan_id и trial_ends_at выбранные админом в форме.
Для юзера
$user-> createSubscription(array_merge(
$request->all(), [
"trial_ends_at" => config('services.subscription.trial_ends_in_days') ? Carbon::today()->addDays(config('services.subscription.trial_ends_in_days')) : null,
]
));
Юзер выбирает plan_id, но не выбирает trial_ends_at он задается на этот случае в конфиге.
Отличия метода создания Subscription для юзера и админа, в том что юзер может и должен в момент создания подписки добавить карту, если ее еще нет. А админ конечно за юзера это сделать не может и если карты у юзера нет добавляет подписку только в БД, а не в шлюзе где карта нужна для автопродления...
Вот и думаю как это все зарефакторить... Какой можно паттерн применить.. Может какой нибудь фабричный метод... Или в класс подписки часть из этого тащить из трейта юзера.
Суть что бы лучше читалось и меньше услоовий и повтора кода. Конечно можно тупо вынести повторящийся код в приватные методы. Жду предложений...
Сделал это таким образом
Конфиг
'subscription' => [
'trial_ends_in_days' => 4,
]
Контроллер в случае если мы эту дату берем из конфига, а не из формы. Те если платит юзер, а не админ, то ему дату не выбирать.
$user-> createSubscription(array_merge(
$request->all(), [
"trial_ends_at" => config('services.subscription.trial_ends_in_days') ? Carbon::today()->addDays(config('services.subscription.trial_ends_in_days')) : null,
]
));
Модель. Трейт юзера, где мы создаем для юзера подписку. От разных форматов даты удалось избавиться, тк я понял что через API можно и просто timestamp передать, узнал у саппорта.
public function createSubscription($request)
{
if (isset($request['payment_method_nonce'])) {
if ($this->gateway_id) {
throw new \LogicException('When user have payment method add subscription only with this method');
} else {
$this->createGatewayCustomerWithPaymentMethod($request['payment_method_nonce']);
}
} else {
if (!$this->gateway_id) {
throw new \LogicException('When user have not payment method add subscription only with add payment data');
}
}
$plan = Plan::findOrFail($request['plan_id']);
if (empty($request['trial_ends_at'])) {
$response = GatewaySubscription::create([
'planId' => $plan->gateway_id,
'paymentMethodToken' => $this->defaultCard->gateway_id,
'options' => ['startImmediately' => true]
]);
} else {
$response = GatewaySubscription::create([
'planId' => $plan->gateway_id,
'paymentMethodToken' => $this->defaultCard->gateway_id,
'firstBillingDate' => $request['trial_ends_at'],
]);
}
if (! $response->success) {
throw new Exception('Gateway failed to create subscription: '.$response->message);
}
return $this->subscriptions()->create([
'gateway_id' => $response->subscription->id,
'card_id' => $this->defaultCard->id,
'current_period_end' => $response->subscription->billingPeriodEndDate,
'plan_id' => $plan->id,
'trial_ends_at' => $request['trial_ends_at'],
]);
}
Ну а в случае с админом, я просто через js календарик в нужном формате дату запрашиваю и ничего так же преобразовывать не приходиться. Все что нужно в Request
public function createSubscriptionByAdmin($request)
{
$plan = Plan::findOrFail($request['plan_id']);
if ($this->hasCard()) {
if (empty($request['trial_ends_at'])) {
$response = GatewaySubscription::create([
'planId' => $plan->gateway_id,
'paymentMethodToken' => $this->defaultCard->gateway_id,
'options' => ['startImmediately' => true]
]);
} else {
$response = GatewaySubscription::create([
'planId' => $plan->gateway_id,
'paymentMethodToken' => $this->defaultCard->gateway_id,
'firstBillingDate' => $request['trial_ends_at'],
]);
}
if (!$response->success) {
throw new Exception('Gateway failed to create subscription: '.$response->message);
}
return $this->subscriptions()->create([
'gateway_id' => $response->subscription->id,
'card_id' => $this->defaultCard->id,
'current_period_end' => $response->subscription->billingPeriodEndDate,
'plan_id' => $plan->id,
'trial_ends_at' => $request['trial_ends_at'],
]);
} else {
return $this->subscriptions()->create([
'plan_id' => $plan->id,
'trial_ends_at' => $request['trial_ends_at'],
]);
}
}
До сегодняшних изменений все это в двух сервисных классах SubscriptionBuilder и GenericSubscriptionBuilder было которыя у cashier пакета подсмотрел. Кода меньше стало. Но осталось эти две метода createSubscription() и createSubscriptionByAdmin() зарефакторить на предмет повторного кода и меньше условий. Благодаря тому что с датой разобрался все куда лучше стало.
Нет, посмотрел свой код мутатор мне не подходит, так как мне нужно менять формат даты не в момент создания объекта именно...
Мне нужно следующее:
1) Принять дату из формы или из конфига
2) Заслать через API дату. Причем там есть варианты и более чистый что ли это через TrialDuration и TrialDurationUnit, а не просто DateTime
3) Использовать дату при создании объекта Subscription
Очевидно что мутатор по крайней мере задачу 2 не решает. Можно конечно написать Хелпер который переводит дату из TimeStamp в TrialDuration и TrialDurationUnit. А перед этим то что в форме перевести в Timestamp
Почему модель не должна знать?
Допустим есть метод
class ExampleModel
{
function create($request)
{
$request['trial_end'] = fromSomeFormatDate($request['trial_end']);
}
И вот этот fromSomeFormatDate() зависит от того, что мы сделали в форме и интерфейсе? Кажется совсем не красиво. Тк в форме то мы захотим в таком формате у юзера брать дату, а затем заказчик скажет некрасиво давайте в чуть другом, а мы уже модель меняем. А в метод create модели данные могут и не только из данной формы поступать...
Про сервис понятно, но допустим его не приминяем... А как в мутаторе? Можешь пример привести?
Спасибо так и сделаю.
А если я хочу из формы принимать дату. То преобразовать ее из того что прислала форма ну например html 5 поле с датой или js календарь итд в Carbon дату где лучше? Мне кажется модель не должна знать что там за дата в форм, значки в контроллере или в реквесте быть может?
Как красивее? В реквесте по любому дату ведь проверять
Хочу прописать дату окончания триального периода подписки по умолчанию.
Где это лучше сделать?
В конфиг дату прописать не получается.
'subscription' => [
'trialEndsAt' => \Carbon::today()->addDays(1),
]
Не работает.
В класс Subscription прописать константу или статическую переменную имеющую значение Carbon::today()->addDays(1) не получается.
Как вообще принято даты по умолчанию прописывать?
Нет конечно можно прописать период по умолчанию через durationUnit и duration, но это две переменные и вообще хочется иметь все таки именно дату по умолчанию.
Как быть???
а проблему твою не понял.ты пишешь$user->subscription->isTrial()а так$subscription->isTrial()
Конечно будет работать и так и сяк. Я скорее о том, что везде будет достаточно $user->subscription->isTrial() в контроллерах.. Так как везде есть юзер...
В общем я понял, что подход верный...
Возник каверзный вопрос по архитектуре.
В SAAS приложении, почти все можно делать в trait класса User, ну или в самом классе, просто trait повышает читаемость, понятно что кроме User никакой другой класс не подключит.
Если брать через отношения то к User относятся несколько классов - все связанное с оплатой и подпискам - его карты, подписки, платежи, так некоторые сервисные вещи, к примеру картинки, файлы юзера итд.
Так вот получается, что можно в контроллерах все время дергать, что то типа $user->subscription->isTrial() $user->card->default $user->mainAvatar() примеры и названия совершенно случайны.. В общем управлять через отношения моделями дочерних юзеру моделей. Ну или запихнуть в сам класс/используемый им трейт, что все эти обращения к дочерним моделям и вызывать типа $user->trialSubscription() $user->defaultCard() $user->mailAvatar().
И все это вместо $subscription->isTrial() $card->default() $avatar->mainJpg(); итд. итп.
Но не противоречит ли все это тому, что класс не должен быть раздут и быть супер-классом? Как с этим бороться. Правильный ли путь более менее сложные фичи связанные с дочерними моделями юзера сгонять в сервисный слой и дергать его все равно через класс/трейт юзера?
Понимаю, что вопрос слишком общий... Интересует подход в целом. Что бы не зайти в тупик...
А я вообще сделал по простому
class AddSubscriptionController extends Controller {
public function __construct() {
$this->middleware('adminUserIsNotSubscriebed');
}
namespace App\Http\Middleware\Admin;
use Closure;
use Illuminate\Support\Facades\Config;
use App\Classes\Models\User;
class UserIsNotSubscriebed
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$user = User::find($request->user_id);
if ($user->subscribed()) {
return redirect()->route('admin.subscription', $request->user_id);
}
return $next($request);
}
}
Если юзер с данными ID имеет подписку то из данной формы посылаем и все.
Но да может с политиками как то и лучше
Ведь, это класс, который проверяет какой-то абстрактный запрос, а мы его связываем с текущим роутом.
Насчет authorize в request я понял что это не какой то абстрактный запрос, а самый что не на есть текущий. Именно запрос который по текущуму роуту идет и который принимаем в методе контроллера. Алексей поправь если не прав?
ачем тебе сохранять price, если она уже есть в Plan?
Цена может быть со скидкой по коду по сравнению с планом. Пока это не нужно, но в кешире заложено когда придется все равно нужно будет прикрутить.
Пусть я сейчас это уберу.. name на случай когда несколько подписок, у нас пока и в обозримом одна, опять же поле можно удалить
card_id в подписках также нужно и скорее и сейчас надо оставить. Тк у user может быть одна карта по умолчанию, но вот именно подписка может продлятся другой.
Можно все убрать. Но делается и немного на вырост...
Кроме того убирание всего этого особо и не упростит.
Главное то продолжительность trial и сам факт его наличия
От вот этого
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,
]);
}
Все равно не уйти. Тк подписку надо создать не только у нас в БД, но и в платежном шлюзе. И там у Api свои особенности то есть вот это все им надо отослать 'trialPeriod', 'trialDurationUnit', 'trialDuration' ну или отсылать в случае если создаем подписку без триала. А просто дату окончания триала в явном виде им отослать нельзя точно.
Те мне по любому нужен сначала запрос к шлюзу. А затем создание модели подписки.
В пакете на основе которого, я все это делал примерно такая логика и есть https://github.com/laravel/cashier-brai … uilder.php https://github.com/laravel/cashier-brai … llable.php
Другое дело, можно ли вообще запихнуть это именно в метод подписки, а не сервиса? Вот в чем вопрос, а даже не в формате массива с данными..
Если лучше сервисный слой оставить то я и оставлю, ну да уберу лишние поля конечно..