Статьи

Laravel и Braintree: промежуточное программное обеспечение и другие передовые концепции

Эта статья была рецензирована Вираджем Хатавкаром . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!


Ранее мы видели, как настроить приложение Laravel для обработки подписок Braintree.

Логотип Брейнтри

На этот раз мы поговорим о том, как:

  • Запретить пользователям подписываться на один и тот же план дважды
  • Добавьте базовые флеш-сообщения в наше приложение
  • Добавить возможность менять планы
  • Создайте промежуточное ПО для защиты некоторых маршрутов в зависимости от статуса подписки.
  • Ограничить пользователям с базовыми подписками доступ к премиум-контенту
  • Отменить и возобновить подписку
  • Добавьте уведомления Braintree к событиям приложения через webhooks

Двойные подписки

В нынешнем виде, если мы зайдем на страницу index планов, мы все равно увидим кнопку « Choose Plan для плана, на который у нас в данный момент подписка, и это не должно иметь место. В представлении index планов добавим if conditional чтобы скрыть кнопку в зависимости от статуса подписки пользователя:

 [...] @if (!Auth::user()->subscribedToPlan($plan->braintree_plan, 'main')) <a href="{{ url('/plan', $plan->slug) }}" class="btn btn-default pull-right">Choose Plan</a> @endif [...] 

Но это не значит, что пользователи не могут получить доступ к плану, введя URL-адрес, указывающий на тот же план в адресной строке. Чтобы противостоять этому, давайте обновим код в действии show PlansController следующим образом:

 [...] public function show(Request $request, Plan $plan) { if ($request->user()->subscribedToPlan($plan->braintree_plan, 'main')) { return redirect('home')->with('error', 'Unauthorised operation'); } return view('plans.show')->with(['plan' => $plan]); } [...] 

Здесь мы получаем пользователя из объекта запроса; помните, что все наши маршруты попадают под промежуточное программное обеспечение аутентификации, и таким образом возможно получить аутентифицированного пользователя. Получив пользователей, мы проверяем, подписаны ли они уже на план. Если это так, мы перенаправляем их на домашнюю страницу и показываем уведомление. Мы внедрим базовые флеш-сообщения позже.

Последнее предостережение — запрещение пользователям отправлять форму оплаты с другим значением plan ID . Можно проверить элементы DOM и изменить значение для скрытого ввода. В нашем SubscriptionsController давайте обновим метод store следующим образом:

 [...] public function store(Request $request) { $plan = Plan::findOrFail($request->plan); if ($request->user()->subscribedToPlan($plan->braintree_plan, 'main')) { return redirect('home')->with('error', 'Unauthorised operation'); } $request->user()->newSubscription('main', $plan->braintree_plan)->create($request->payment_method_nonce); // redirect to home after a successful subscription return redirect('home')->with('success', 'Subscribed to '.$plan->braintree_plan.' successfully'); } [...] 

Flash-сообщения

Теперь давайте реализуем некоторые базовые флеш-сообщения для отображения уведомлений в приложении в ответ на определенные операции. В файле resources/views/layouts/app.blade.php давайте resources/views/layouts/app.blade.php этот блок прямо над нашим содержимым:

 [...] <div class="container"> <div class="row"> <div class="col-md-8 col-md-offset-2"> @include ('layouts.partials._notifications') </div> </div> </div> @yield('content') [...] 

Затем мы создаем частичное уведомление:

ресурсы / виды / макеты / обертоны / _notifications.blade.php

 @if (session('success')) <div class="alert alert-success"> {{ session('success') }} </div> @endif @if (session('error')) <div class="alert alert-danger"> {{ session('error') }} </div> @endif 

В частности, мы использовали помощника session чтобы помочь нам придумать разные цвета уведомлений, основанные на статусе сеанса, то есть success или error .

Планы обмена

После того, как пользователь подписался на наше приложение, он может иногда захотеть перейти на новый тарифный план.

Для этого сначала нужно проверить, подписан ли пользователь на какой-либо план внутри метода store в SubscriptionsController . Если нет, мы подписываем их на новый план. Чтобы обменять пользователя на новую подписку, мы передаем идентификатор плана методу swap .

Давайте откроем наш SubscriptionsController и обновим метод store :

 public function store(Request $request) { [...] if (!$request->user()->subscribed('main')) { $request->user()->newSubscription('main', $plan->braintree_plan)->create($request->payment_method_nonce); } else { $request->user()->subscription('main')->swap($plan->braintree_plan); } return redirect('home')->with('success', 'Subscribed to '.$plan->braintree_plan.' successfully'); } 

Давайте посмотрим, все ли работает как задумано, выбрав другой план, заполнив поддельные данные карты (те же данные, которые использовались при подписке на новый план в предыдущем посте), а затем отправив форму. Если мы посмотрим в таблицу subscriptions , мы должны заметить, что план для аутентифицированного пользователя изменился. Это изменение также должно быть отражено в Braintree по сделкам. Далее мы хотим защитить маршруты на основе статуса подписки.

Защита маршрутов

Для этого приложения мы хотим ввести уроки. Только подписанные пользователи должны иметь доступ к урокам. Мы сгенерируем LessonsController :

 php artisan make:controller LessonsController 

… Затем перейдите к контроллеру и создайте действие index . Мы не будем создавать представления для этого, просто отображаем текст:

 [...] public function index() { return 'Normal Lessons'; } [...] 

Затем мы создаем маршрут, указывающий на это действие индекса в группе маршрутов с auth middleware :

 Route::group(['middleware' => 'auth'], function () { Route::get('/lessons', 'LessonsController@index'); }); 

Давайте также обновим нашу панель навигации, чтобы включить ссылку на эти «уроки»:

ресурсы / просмотров / макеты / app.blade.php

 [...] <ul class="nav navbar-nav navbar-left"> <li><a href="{{ url('/plans') }}">Plans</a></li> <li><a href="{{ url('/lessons') }}">Lessons</a></li> </ul> [...] 

Если мы теперь нажмем « Lessons на панели навигации, мы увидим текст «Обычные уроки» независимо от статуса подписки. Это не должно быть так. Только подписанные пользователи должны иметь доступ к урокам. Чтобы включить это поведение, нам нужно создать промежуточное программное обеспечение:

 php artisan make:middleware Subscribed 

Давайте откроем файл в app/Http/Middleware/Subscribed.php и обновим метод handle так:

 [...] public function handle($request, Closure $next) { if (!$request->user()->subscribed('main')) { if ($request->ajax() || $request->wantsJson()) { return response('Unauthorized.', 401); } return redirect('/plans'); } return $next($request); } [...] 

В приведенном выше блоке кода мы проверяем пользователей, которые не подписаны на какой-либо тарифный план. Если это так, независимо от того, используете ли вы ajax или нет, они получат Unauthorized ответ или будут перенаправлены на index страницу планов. Если пользователь уже подписан на план, мы просто перейдем к следующему запросу.

С этим определением давайте перейдем к app/Http/Kernel.php и зарегистрируем наше промежуточное ПО в $routeMiddleware чтобы мы могли вызывать промежуточное ПО в пределах наших маршрутов:

приложение / Http / Kernel.php

 protected $routeMiddleware = [ [...] 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'subscribed' => \App\Http\Middleware\Subscribed::class, ]; 

Теперь мы можем создать другую группу маршрутов внутри группы с промежуточным программным обеспечением auth , но на этот раз мы передаем subscribed как промежуточное программное обеспечение. Именно внутри этой новой группы маршрутов мы определим маршрут для доступа к урокам:

 Route::group(['middleware' => 'auth'], function () { [...] Route::group(['middleware' => 'subscribed'], function () { Route::get('/lessons', 'LessonsController@index'); }); }); 

Если мы попытаемся зарегистрировать нового пользователя и нажать « Lessons на панели навигации, мы будем перенаправлены на /plans plan. Если мы подписываемся на план с новой учетной записью, у нас теперь есть доступ к урокам.

Защита премиум-контента от основных пользователей

Для нашего приложения мы хотим предоставить базовый и платный контент. Пользователи с базовыми планами будут иметь доступ к обычным урокам, но только пользователи с платными подписками будут иметь доступ к платным урокам. Для демонстрации давайте создадим premium метод внутри LessonsController :

 [...] public function premium() { return 'Premium Lessons'; } [...] 

Давайте также обновим панель навигации, добавив ссылку на Premium Lessons :

 [...] <ul class="nav navbar-nav navbar-left"> <li><a href="{{ url('/plans') }}">Plans</a></li> <li><a href="{{ url('/lessons') }}">Lessons</a></li> <li><a href="{{ url('/prolessons') }}">Premium Lessons</a></li> </ul> [...] 

Мы могли бы разместить маршрут для доступа к платным урокам в группе маршрутов с помощью subscribed промежуточного программного обеспечения, но это позволит любому получить доступ к премиум-контенту независимо от того, подписаны они или нет. Нам нужно будет создать другое промежуточное программное обеспечение, чтобы предотвратить доступ пользователей к контенту премиум-класса:

 php artisan make:middleware PremiumSubscription 

Код, PremiumSubscription промежуточное программное обеспечение PremiumSubscription , не будет так сильно отличаться от того, который мы имели в промежуточном программном обеспечении с PremiumSubscription . Единственное отличие состоит в том, что мы должны быть конкретными при проверке статуса подписки пользователя. Если пользователь не подписан на премиум-план, он будет перенаправлен на index страницу плана:

приложение / Http / Промежуточный / Premium.php

 [...] public function handle($request, Closure $next) { if (!$request->user()->subscribed('premium', 'main')) { if ($request->ajax() || $request->wantsJson()) { return response('Unauthorized.', 401); } return redirect('/plans'); } return $next($request); } [...] 

Давайте зарегистрируем это новое промежуточное ПО в Ядре, прежде чем создавать новую группу маршрутов для премиум-пользователей:

приложение / Http / Kernel.php

 protected $routeMiddleware = [ [...] 'subscribed' => \App\Http\Middleware\Subscribed::class, 'premium-subscribed' => \App\Http\Middleware\PremiumSubscription::class, ]; 

Затем мы создаем новую группу маршрутов чуть ниже группы с subscribed промежуточным программным обеспечением:

 Route::group(['middleware' => 'auth'], function () { [...] Route::group(['middleware' => 'premium-subscribed'], function () { Route::get('/prolessons', 'LessonsController@premium'); }); }); 

Вот и все. Любой пользователь с базовой подпиской не может получить доступ к платному контенту, но премиум-пользователи могут получить доступ как к платному, так и к базовому контенту. Далее мы рассмотрим, как отменить и возобновить подписку.

Отмена подписок

Чтобы включить отмену и возобновление подписки, нам нужно будет создать новую страницу. Давайте откроем наш SubscriptionsController и добавим действие index . Он находится внутри действия index где мы вернем представление для управления подписками:

 [...] class SubscriptionController extends Controller { public function index() { return view('subscriptions.index'); } [...] } 

Давайте сначала создадим папку подписок внутри представлений перед созданием представления index . Затем мы вставляем фрагмент ниже в представление:

ресурсы / виды / подписки / index.blade.html

 @extends('layouts.app') @section('content') <div class="container"> <div class="row"> <div class="col-md-8 col-md-offset-2"> <div class="panel panel-default"> <div class="panel-heading">Manage Subscriptions</div> <div class="panel-body"> ... </div> </div> </div> </div> </div> @endsection 

Затем мы должны определить маршрут, указывающий на это представление. Этот маршрут должен быть внутри группы маршрутов с subscribed промежуточным ПО:

 Route::group(['middleware' => 'subscribed'], function () { Route::get('/lessons', 'LessonsController@index'); Route::get('/subscriptions', 'SubscriptionController@index'); }); 

Давайте обновим выпадающий список в панели навигации, чтобы включить ссылку, указывающую на только что созданное представление index (поместите ссылку над ссылкой выхода из системы) . Эта ссылка должна быть видна только подписчикам:

ресурсы / просмотров / макеты / app.blade.html

 <ul class="dropdown-menu" role="menu"> [...] <li> @if (Auth::user()->subscribed('main')) <a href="{{ url('/subscriptions') }}"> Manage subscriptions </a> @endif </li> [...] </ul> 

Следующим шагом является создание ссылки, позволяющей пользователям отменять свои подписки. Для этого нам понадобится форма. Форма важна, так как нам нужно использовать защиту CSRF, чтобы убедиться, что пользователь, отменяющий подписку, действительно является правильным пользователем. Давайте создадим форму внутри div с классом panel-body :

ресурсы / виды / подписки / index.blade.html

 [...] <div class="panel-body"> @if (Auth::user()->subscription('main')->cancelled()) <!-- Will create the form to resume a subscription later --> @else <p>You are currently subscribed to {{ Auth::user()->subscription('main')->braintree_plan }} plan</p> <form action="{{ url('subscription/cancel') }}" method="post"> <button type="submit" class="btn btn-default">Cancel subscription</button> {{ csrf_field() }} </form> @endif </div> [...] 

Обратите внимание, что у нас есть условие, чтобы проверить, есть ли у пользователя отмененная подписка, и если это так, мы оставим им возможность возобновить ее. Если у пользователя по-прежнему есть активная подписка, тогда появится форма для отмены.

Когда подписка отменяется, Касса автоматически установит столбец ends_at в нашей базе данных. Этот столбец используется, чтобы знать, когда subscribed метод должен начать возвращать false . Например, если клиент отменяет подписку 1 марта, но срок ее подписки не запланирован до March 5th , метод subscribed будет возвращать значение true до 5 марта.

Имея форму для отмены подписок, мы можем теперь определить различные действия контроллера и маршруты для обработки процесса отмены.

В SubscriptionsController давайте добавим метод cancel :

 [...] class SubscriptionController extends Controller { public function cancel(Request $request) { $request->user()->subscription('main')->cancel(); return redirect()->back()->with('success', 'You have successfully cancelled your subscription'); } [...] } 

Здесь мы отбираем пользователя у объекта запроса, получаем его подписку, а затем вызываем метод cancel Кассира для этой подписки. После этого мы перенаправляем пользователя обратно на ту же страницу, но с уведомлением об отмене.

Нам также нужен маршрут для обработки действия отправки формы:

 Route::group(['middleware' => 'subscribed'], function () { [...] Route::post('/subscription/cancel', 'SubscriptionController@cancel'); }); 

Возобновление подписок

Пользователь все еще должен иметь «льготный период», чтобы возобновить подписку. Если пользователь отменяет подписку, а затем возобновляет эту подписку до того, как срок действия подписки полностью истек, счета не будут немедленно выставлены. Вместо этого их подписка будет просто повторно активирована, и им будет выставлен счет в исходном биллинговом цикле.

Процедура возобновления подписки аналогична той, что мы делали при отмене подписки. Единственное отличие состоит в том, что мы вызываем метод resume подписки вместо метода cancel . Мы также уведомим пользователя, когда льготный период должен закончиться в верхней части формы.

Давайте обновим представление resources/views/subscriptions/index.blade.html в него форму, позволяющую пользователям возобновлять подписки:

 <div class="panel-body"> @if (Auth::user()->subscription('main')->cancelled()) <p>Your subscription ends on {{ Auth::user()->subscription('main')->ends_at->format('dS M Y') }}</p> <form action="{{ url('subscription/resume') }}" method="post"> <button type="submit" class="btn btn-default">Resume subscription</button> {{ csrf_field() }} </form> @else [...] 

Затем мы создаем действие контроллера и маршруты для обработки действия отправки формы:

приложение / HTTP / Контроллеры / SubscriptionsController.php

 [...] class SubscriptionController extends Controller { [...] public function resume(Request $request) { $request->user()->subscription('main')->resume(); return redirect()->back()->with('success', 'You have successfully resumed your subscription'); } [...] } 

Давайте обновим наши маршруты, чтобы учесть действие отправки формы:

 Route::group(['middleware' => 'subscribed'], function () { [...] Route::post('/subscription/cancel', 'SubscriptionController@cancel'); Route::post('/subscription/resume', 'SubscriptionController@resume'); }); 

Как вы можете видеть, Касса чрезвычайно облегчает управление подписками. Далее мы рассмотрим, как настроить webhooks для нашего приложения.

Webhooks

Stripe и Braintree могут уведомить наше приложение о различных событиях через веб-хуки. Webhook — это часть вашего приложения, которую можно вызывать, когда действие происходит на стороннем сервисе или каком-либо другом сервере. Проще говоря, как наше приложение узнает, была ли отклонена чья-то карта, когда мы пытаемся взимать с них плату за членство, или срок действия их карты истекает, или что-то еще идет не так? Когда это происходит на Braintree, мы хотим, чтобы наше приложение знало об этом, чтобы мы могли отменить подписку.

Для работы с веб-крюками Брейнтри мы определяем маршрут, который указывает на контроллер веб-крючка Кассира. Этот контроллер будет обрабатывать все входящие запросы webhook и отправлять их в соответствующий метод контроллера:

 Route::post( 'braintree/webhooks', '\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook' ); 

Обратите внимание, что маршрут не находится внутри какой-либо группы маршрутов, так как для этого нам не нужно проходить аутентификацию.

По умолчанию WebhookController будет автоматически обрабатывать отмену подписок, у которых слишком много неудачных платежей (как определено вашими настройками Braintree); тем не менее, вы можете расширить этот контроллер для обработки любого события webhook, которое вам нравится. Вы можете прочитать больше о том, как расширить WebhookController здесь .

После того, как мы зарегистрировали этот маршрут, давайте настроим URL-адрес webhook в настройках панели управления Braintree. Есть множество событий, о которых мы можем выбрать, чтобы получать уведомления, но для этого урока мы просто придерживаемся отмененных:

Webhooks и CSRF Защита

Поскольку веб-крюкам Braintree необходимо обойти защиту Laravel от CSRF, мы должны указать URI как исключение в нашем промежуточном программном обеспечении VerifyCsrfToken или перечислить маршрут вне группы веб-промежуточного программного обеспечения:

приложение / Http / Промежуточное / VerifyCsrfToken.php

 protected $except = [ 'braintree/*', ]; 

Тестирование Webhooks

До сих пор мы разрабатывали наше приложение локально, и получить доступ к приложению с другого компьютера невозможно.

Чтобы проверить, работают ли веб-зацепки так, как задумано, нам нужно будет использовать что-то вроде Ngrok для обслуживания приложения.

Ngrok — это удобный инструмент и сервис, который позволяет нам туннелировать запросы из широко открытого Интернета на нашу локальную машину, когда она находится за NAT или межсетевым экраном. Обслуживание приложения через Ngrok также предоставит нам URL-адрес, который позволяет любому получить доступ к нашему приложению с другого компьютера. Давайте обновим URL веб-крючка Брейнтри, чтобы использовать этот новый URL:

Мы можем проверить это, вручную отменив подписку для пользователя из панели управления Braintree. После того как мы отменим подписку, Braintree отправит эти данные в только что созданный веб-крючок, а остальная часть обработает кассир.

Если мы теперь посмотрим на данные в таблице подписок, то заметим столбец ends_at для пользователя, чья подписка, которую мы только что отменили, была обновлена.

Вывод

Мы прошли долгий путь, чтобы добраться до этой точки. Наше приложение теперь поддерживает функции подписки, доступные на большинстве сайтов. Возможно, мы не исчерпали все методы, предлагаемые Cashier, но, как вы могли заметить, Cashier чрезвычайно облегчает управление подписками Braintree, тем самым экономя разработчикам много времени.

Касса не ограничена Braintree, поскольку она также поддерживает Stripe . Если вы считаете, что мы не рассмотрели некоторые из наиболее важных функций или у вас есть собственные реализации для демонстрации, сообщите нам об этом в комментариях!