Русское сообщество разработки на PHP-фреймворке Laravel.
Ты не вошёл. Вход тут.
Допустим нужно создать объект в простом случае это просто 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,
]);
}
}
Интересно какие еще есть хорошие практики создания объекта со сложными условиями и дополнительными действиями? Возможно преобразователи?
Изменено htclog81 (01.12.2017 10:17:24)
Не в сети
Старайся избегать подобного преобразования данных, ну или старайся хотя бы минимизировать эти преобразования. Например, у тебя есть явная дубликация данных. Зачем тебе сохранять price, если она уже есть в Plan? То же самое с name, card_id, trial_ends_at и пр.
Возможно преобразователи?
Нет, разве что для вещей вроде trial_ends_at
Не в сети
ачем тебе сохранять 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
Другое дело, можно ли вообще запихнуть это именно в метод подписки, а не сервиса? Вот в чем вопрос, а даже не в формате массива с данными..
Если лучше сервисный слой оставить то я и оставлю, ну да уберу лишние поля конечно..
Изменено htclog81 (01.12.2017 14:53:31)
Не в сети
В общем в итоге получилось вот что. В зависимости от условий создания 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 для юзера и админа, в том что юзер может и должен в момент создания подписки добавить карту, если ее еще нет. А админ конечно за юзера это сделать не может и если карты у юзера нет добавляет подписку только в БД, а не в шлюзе где карта нужна для автопродления...
Вот и думаю как это все зарефакторить... Какой можно паттерн применить.. Может какой нибудь фабричный метод... Или в класс подписки часть из этого тащить из трейта юзера.
Суть что бы лучше читалось и меньше услоовий и повтора кода. Конечно можно тупо вынести повторящийся код в приватные методы. Жду предложений...
Не в сети