Laravel по-русски

Русское сообщество разработки на PHP-фреймворке Laravel.

Ты не вошёл. Вход тут.

#1 01.12.2017 10:06:15

htclog81
Откуда: Москва
Сообщений: 192
Сайт

Нужна хорошая практика создания Eloquent объектов.

Допустим нужно создать объект в простом случае это просто 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)

Не в сети

#2 01.12.2017 12:23:38

Re: Нужна хорошая практика создания Eloquent объектов.

Старайся избегать подобного преобразования данных, ну или старайся хотя бы минимизировать эти преобразования. Например, у тебя есть явная дубликация данных. Зачем тебе сохранять price, если она уже есть в Plan? То же самое с name, card_id, trial_ends_at и пр.

Возможно преобразователи?

Нет, разве что для вещей вроде trial_ends_at

Не в сети

#3 01.12.2017 14:52:54

htclog81
Откуда: Москва
Сообщений: 192
Сайт

Re: Нужна хорошая практика создания Eloquent объектов.

ачем тебе сохранять 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)

Не в сети

#4 03.12.2017 23:37:52

htclog81
Откуда: Москва
Сообщений: 192
Сайт

Re: Нужна хорошая практика создания Eloquent объектов.

В общем в итоге получилось вот что. В зависимости от условий создания 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 для юзера и админа, в том что юзер может и должен в момент создания подписки добавить карту, если ее еще нет. А админ конечно за юзера это сделать не может и если карты у юзера нет добавляет подписку только в БД, а не в шлюзе где карта нужна для автопродления...

Вот и думаю как это все зарефакторить... Какой можно паттерн применить.. Может какой нибудь фабричный метод... Или в класс подписки часть из этого тащить из трейта юзера.

Суть что бы лучше читалось и меньше услоовий и повтора кода. Конечно можно тупо вынести повторящийся код в приватные методы. Жду предложений...

Не в сети

Подвал раздела