Статьи

Ларавел и Брейнтри, сидящие на дереве…

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


Подписки на онлайн-сервисы встречаются очень часто — от подписки на сервисы потоковой передачи музыки до обучающих сайтов и доступа к премиум-контенту .

С Laravel 5 мы увидели Laravel Cashier , официальный пакет Laravel, который помогает разработчикам управлять услугами биллинга по подписке Stripe и Braintree без написания большей части стандартного кода биллинга по подписке.

Stripe и Braintree — это платежные платформы, которые позволяют легко принимать платежи в вашем приложении или на веб-сайте.

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

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

В первой части этой обширной серии из двух частей мы собираемся:

  • Настройте Laravel Cashier
  • Зарегистрируйтесь в песочнице Braintree (для производственных приложений мы используем основной сервис Braintree)
  • Создавайте планы на Braintree
  • Создать команду ремесленника для синхронизации онлайн-планов с нашей базой данных
  • Разрешить пользователям подписываться на план

Во второй части мы будем:

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

Полный код для первой части можно найти здесь .

Создание приложения

Начнем со свежей установки Laravel.

 composer create-project laravel/laravel lara-billable 

Подготовка базы данных

Затем мы должны настроить базу данных перед выполнением любых миграций. Давайте использовать MySQL — он прост в использовании, прост в настройке и включен в хорошо построенную среду разработки, такую ​​как Homestead Improved . После настройки базы данных мой файл .env выглядит так:

 DB_HOST=localhost DB_DATABASE=homestead DB_USERNAME=homestead DB_PASSWORD=secret 

Строительные леса Auth

Следующий шаг — добавление аутентификации в наше приложение.

Laravel представила встроенный модуль аутентификации в версии 5.2, что сделало это чрезвычайно простым.

 php artisan make:auth 

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

Чтобы иметь возможность зарегистрироваться и войти в систему, нам нужны таблицы для хранения пользовательских данных. Если мы посмотрим на database/migrations , мы заметим, что созданное приложение Laravel поставляется с миграциями для создания таблиц users и password_resets . Давайте запустим эти миграции:

 php artisan migrate 

Если мы перейдем к /register , мы можем зарегистрировать новых пользователей. Ссылки для регистрации и входа также присутствуют в навигационной панели.

Настройка кассира

С таблицей пользователей мы можем добавить Кассу. Поскольку мы будем использовать Braintree для этого урока, нам потребуется пакет braintree-cashier :

 composer require laravel/cashier-braintree 

Затем мы регистрируем Laravel\Cashier\CashierServiceProvider в нашем config/app.php :

 'providers' => [ // Other service providers... Laravel\Cashier\CashierServiceProvider::class, ], 

Далее нам нужно Billable черту Billable в модели User чтобы иметь возможность вызывать различные методы кассира для пользователя:

 [...] use Laravel\Cashier\Billable; [...] class User extends Authenticatable { use Billable; [...] } 

Затем мы добавляем дополнительные столбцы в таблицу users для выставления счетов. Мы также создадим таблицу subscriptions для обработки всех наших подписок:

 php artisan make:migration add_billable_columns_to_users_table --table=users 

Откройте вновь созданную миграцию и измените метод up на этот:

 public function up() { Schema::table('users', function (Blueprint $table) { $table->string('braintree_id')->nullable(); $table->string('paypal_email')->nullable(); $table->string('card_brand')->nullable(); $table->string('card_last_four')->nullable(); $table->timestamp('trial_ends_at')->nullable(); }); } 

Давайте теперь создадим модель subscription и миграции:

 php artisan make:model Subscription -m 

Откройте миграцию и настройте метод up следующим образом:

 public function up() { Schema::table('subscriptions', function (Blueprint $table) { $table->increments('id'); $table->integer('user_id'); // A subscription belongs to a user $table->string('name'); // The name of the subscription $table->string('braintree_id'); //id for the subscription $table->string('braintree_plan'); // The name of the plan $table->integer('quantity'); $table->timestamp('trial_ends_at')->nullable(); $table->timestamp('ends_at')->nullable(); $table->timestamps(); }); } 

С этой настройкой выполните команду migrate Artisan, чтобы создать таблицу subscriptions и добавить дополнительные столбцы в таблицу users :

 php artisan migrate 

На данный момент у нас есть настроенная сторона Laravel. Время связывать вещи на конце Брэйнтри. Мы будем использовать песочницу Braintree, поскольку это не производственное приложение. Для тех, у кого нет учетной записи Sandbox, зарегистрируйтесь здесь , затем войдите.

Оказавшись внутри панели инструментов, сгенерируйте новый ключ API, чтобы иметь возможность использовать API Braintree в нашем приложении:

Генерация нового ключа API

После генерации ключа мы также получаем Public Key , Environment Key и Merchant ID . С их помощью нам нужно настроить конфигурацию в нашем приложении Laravel, чтобы мы могли взаимодействовать с API Braintree. Откройте файл .env и установите ключи вместе с их соответствующими значениями. У меня .env файл .env который выглядит так:

 BRAINTREE_ENV=sandbox BRAINTREE_MERCHANT_ID=xxxxxxxxxxxxxx BRAINTREE_PUBLIC_KEY=xxxxxxxxxxxxxx BRAINTREE_PRIVATE_KEY=xxxxxxxxxxxxxx 

Затем мы добавляем конфигурацию Braintree в наш файл config/services.php :

 'braintree' => [ 'model' => App\User::class, //model used to processs subscriptions 'environment' => env('BRAINTREE_ENV'), 'merchant_id' => env('BRAINTREE_MERCHANT_ID'), 'public_key' => env('BRAINTREE_PUBLIC_KEY'), 'private_key' => env('BRAINTREE_PRIVATE_KEY'), ], 

В качестве последнего шага перед установкой связи с API Braintree, давайте добавим следующие вызовы Braintree SDK в метод boot нашего поставщика услуг AppServiceProvider . Мы будем использовать помощник env чтобы получить значения, которые мы установили в нашем файле .env . Обратите внимание, что мы также должны импортировать класс Braintree_Configuration в наш AppServiceProvider , иначе мы не сможем вызывать различные методы из класса Braintree_Configuration :

 [...] use Braintree_Configuration; [...] public function boot() { Braintree_Configuration::environment(env('BRAINTREE_ENV')); Braintree_Configuration::merchantId(env('BRAINTREE_MERCHANT_ID')); Braintree_Configuration::publicKey(env('BRAINTREE_PUBLIC_KEY')); Braintree_Configuration::privateKey(env('BRAINTREE_PRIVATE_KEY')); // Cashier::useCurrency('eur', '€'); } 

Валюта кассира по умолчанию — доллары США (USD). Мы можем изменить валюту по умолчанию, вызвав метод Cashier::useCurrency из boot метода одного из поставщиков услуг. Метод useCurrency принимает два строковых параметра: валюта и символ валюты. В этом уроке мы просто будем придерживаться USD. У меня есть закомментированная строка кода, иллюстрирующая, например, как перейти на евро.

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

Создание планов и их синхронизация с нашей базой данных

Наш следующий шаг — создание планов. Для этого урока мы создадим два плана: базовый и премиум. Давайте перейдем к приборной панели Брейнтри и сделаем это. Обратите внимание, что пробный период не является обязательным, но я установил мой на 14 дней:

План создания

Продолжение:

План создания

Повторите тот же процесс, чтобы создать премиальный план, но установите сумму в 20 долларов США для подписки.

Закончив создание планов на Braintree, мы можем создать таблицу планов для локального хранения планов. Давайте сгенерируем модель Plan и миграцию:

 php artisan make:model Plan -m 

Обновите метод up в вашей миграции следующим образом:

 [...] public function up() { Schema::create('plans', function (Blueprint $table) { $table->increments('id'); $table->string('name'); $table->string('slug')->unique(); //name used to identify plan in the URL $table->string('braintree_plan'); $table->float('cost'); $table->text('description')->nullable(); $table->timestamps(); }); } [...] 

Создайте таблицу plans , запустив миграцию:

 php artisan migrate 

Синхронизация планов

Далее мы хотим заполнить таблицу « Plans данными, которые мы установили на Braintree. Допускается жесткое кодирование планов в нашей таблице, но я нахожу это немного утомительным, особенно когда информация о планах в Интернете постоянно меняется. Чтобы упростить процесс, мы создадим команду ремесленника для синхронизации с планами онлайн и обновим нашу базу данных:

 php artisan make:command SyncPlans 

Для тех, кто не работает на Laravel 5.3+:

 php artisan make:console SyncPlans 

В app/Console/Commands/SyncPlans.php нам нужно изменить значение подписи, а также добавить описание для команды:

 [...] protected $signature = 'braintree:sync-plans'; protected $description = 'Sync with online plans on Braintree'; [...] 

Затем мы регистрируем команду в ядре, чтобы можно было запустить ее из терминала:

 class Kernel extends ConsoleKernel { [...] protected $commands = [ Commands\SyncPlans::class, ]; [...] } 

Если мы сейчас запустим php artisan , команда plan-syncing будет видна из списка доступных команд:

Команда от терминала

Тогда большой вопрос, что должно произойти после запуска этой команды? Команда должна очистить данные в таблице plans , а затем заполнить таблицу данными плана, доступными онлайн. Эта логика должна быть помещена в метод handle в app/Console/Commands/SyncPlans.php :

 [...] use Braintree_Plan; use App\Plan; [...] class SyncBraintreePlans extends Command { [...] public function handle() { // Empty table Plan::truncate(); // Get plans from Braintree $braintreePlans = Braintree_Plan::all(); // uncomment the line below to dump the plans when running the command // var_dump($braintreePlans); // Iterate through the plans while populating our table with the plan data foreach ($braintreePlans as $braintreePlan) { Plan::create([ 'name' => $braintreePlan->name, 'slug' => str_slug($braintreePlan->name), 'braintree_plan' => $braintreePlan->id, 'cost' => $braintreePlan->price, 'description' => $braintreePlan->description, ]); } } } 

Обратите внимание, что мы должны использовать пространства имен Braintree_Plan и App\Plan чтобы иметь возможность статически вызывать методы из этих классов.

Затем мы должны перейти к модели Plan и добавить name , slug , braintree_plan , cost и description в список массовых назначаемых атрибутов. Если мы этого не сделаем, мы получим MassAssignmentException при попытке обновить атрибуты:

 class Plan extends Model { [...] protected $fillable = ['name', 'slug', 'braintree_plan', 'cost', 'description']; [...] } 

Затем мы можем запустить нашу команду, чтобы увидеть, все ли работает как положено:

 php artisan braintree:sync-plans 

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

 php artisan down php artisan braintree:sync-plans php artisan up 

Отображение планов

Давайте теперь отобразим планы на странице. Начнем с создания маршрута:

маршруты / web.php

 [...] Route::get('/plans', 'PlansController@index'); 

Затем мы создаем PlansController и добавляем действие index :

 php artisan make:controller PlansController 

Действие index должно вернуть представление, в котором перечислены все планы:

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

 [...] use App\Plan; [...] class PlansController extends Controller { public function index() { return view('plans.index')->with(['plans' => Plan::get()]); } } 

Теперь давайте настроим вид. Создайте папку планов перед созданием представления index :

 mkdir resources/views/plans touch resources/views/plans/index.blade.php 

В представлении вставьте следующий код:

ресурсы / виды / планы / index.blade.php

 @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">Choose your plan</div> <div class="panel-body"> <ul class="list-group"> @foreach ($plans as $plan) <li class="list-group-item clearfix"> <div class="pull-left"> <h4>{{ $plan->name }}</h4> <h4>${{ number_format($plan->cost, 2) }} monthly</h4> @if ($plan->description) <p>{{ $plan->description }}</p> @endif </div> <a href="#" class="btn btn-default pull-right">Choose Plan</a> </li> @endforeach </ul> </div> </div> </div> </div> </div> @endsection 

Мы использовали помощник number_format чтобы помочь нам отобразить сумму до двух десятичных знаков. В настоящее время кнопка « Choose Plan никуда не ведет, но мы исправим это через мгновение. Если мы сейчас посетим /plans , все планы будут отображаться. Обновите панель навигации, чтобы включить ссылку, указывающую на планы:

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

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

Ваше мнение должно теперь выглядеть так:
Планы в навигации

Нашим следующим шагом будет создание формы оплаты, где пользователи будут заполнять данные своей кредитной карты перед подпиской на план.

Форма оплаты картой

Для этого мы будем использовать Drop-in-интерфейс Braintree. Дополнительную документацию о том, как настроить Drop-in UI, можно найти здесь .

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

маршруты / web.php

 [...] Route::group(['middleware' => 'auth'], function () { Route::get('/plan/{plan}', 'PlansController@show'); }); 

Затем создайте действие show внутри нашего PlansController :

 class PlansController extends Controller { [...] public function show(Plan $plan) { return view('plans.show')->with(['plan' => $plan]); } } 

Поскольку мы хотим передать значение slug в URL-адресе вместо идентификатора плана , мы должны переопределить метод getRouteKeyName в модели Eloquent. По умолчанию при использовании привязки модели маршрута в Laravel возвращается id . Давайте изменим это, чтобы вместо этого мы могли получить значение slug:

Приложение / Plan.php

 public function getRouteKeyName() { return 'slug'; } 

URL-адрес «шоу» теперь должен иметь следующий формат: /plan/{slug} . Время, когда мы создали представление, содержащее форму оплаты:

ресурсы / виды / планы / show.blade.php

 @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">{{ $plan->name }}</div> <div class="panel-body"> .... </div> </div> </div> </div> </div> @endsection 

Мы можем проверить вещи, посетив /plan/premium или /plan/basic , в зависимости от названия плана. Давайте также сделаем так, чтобы кнопка на странице индекса планов указывала на представление show :

ресурсы / виды / планы / index.blade.php

 [...] <a href="{{ url('/plan', $plan->slug) }}" class="btn btn-default pull-right">Choose Plan</a> [...] 

На данный момент, представление является очень простым и не позволяет клиентам вводить данные кредитной карты. Чтобы загрузить Drop-in-интерфейс Braintree, нам потребуется braintree.js после раздела содержимого:

ресурсы / виды / планы / show.blade.php

 @section('braintree') <script src="https://js.braintreegateway.com/js/braintree-2.30.0.min.js"></script> @endsection 

Затем убедитесь, что скрипт загружен в приложение:

ресурсы / мнение / планы / макеты / app.blade.php

 [...] <!-- Scripts --> <script src="/js/app.js"></script> @yield('braintree') [...] 

Обновите представление представления, чтобы включить форму. CLIENT-TOKEN-FROM-SERVER требуется, чтобы это работало, но мы разберемся с этим позже:

ресурсы / виды / планы / show.blade.php

 @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">{{ $plan->name }}</div> <div class="panel-body"> <form> <div id="dropin-container"></div> <hr> <button id="payment-button" class="btn btn-primary btn-flat" type="submit">Pay now</button> </form> </div> </div> </div> </div> </div> @endsection @section('braintree') <script src="https://js.braintreegateway.com/js/braintree-2.30.0.min.js"></script> <script> braintree.setup('CLIENT-TOKEN-FROM-SERVER', 'dropin', { container: 'dropin-container' }); </script> @endsection 

Если мы заходим в /plans/{slug-value} или нажимаем кнопку « Choose Plan на странице списка планов, мы должны перейти к представлению, которое выглядит следующим образом:

Базовый или премиальный план листинга

Однако мы не хотим, чтобы кнопка « Pay Now присутствовала, когда форма оплаты еще не загружена. Для этого мы добавим скрытый класс к кнопке, а затем удалим скрытый класс, как только появится форма оплаты:

ресурсы / виды / планы / show.blade.php

 <form> [...] <button id="payment-button" class="btn btn-primary btn-flat hidden" type="submit">Pay now</button> [...] </form> 

Давайте теперь создадим новый контроллер, отвечающий за генерацию токена. Затем мы будем использовать ajax для установки сгенерированного токена в качестве значения CLIENT-TOKEN-FROM-SERVER . Обратите внимание, что braintree.js нужен токен клиента, сгенерированный SDK сервера Braintree для загрузки формы оплаты:

 php artisan make:controller BraintreeTokenController 

Затем внутри контроллера мы создаем действие token который возвращает ответ JSON, содержащий токен:

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

 [...] use Braintree_ClientToken; [...] class BraintreeTokenController extends Controller { public function token() { return response()->json([ 'data' => [ 'token' => Braintree_ClientToken::generate(), ] ]); } } 

Не забудьте Braintree_ClientToken пространство имен Braintree_ClientToken в этот контроллер; в противном случае метод generate отвечающий за создание токена, выдаст ошибку.

Затем мы обновляем наши маршруты так, чтобы мы могли получить доступ к URL-адресу, содержащему ответ JSON от метода token :

маршруты / web.php

 Route::group(['middleware' => 'auth'], function () { Route::get('/plan/{plan}', 'PlansController@show'); Route::get('/braintree/token', 'BraintreeTokenController@token'); }); 

Попробуйте посетить /braintree/token и значение токена будет отображено на странице. Нашим следующим шагом будет добавление значения токена с помощью AJAX в представление шоу. Обновите код в разделе braintree:

ресурсы / виды / планы / show.blade.php

 @section('braintree') <script src="https://js.braintreegateway.com/js/braintree-2.30.0.min.js"></script> <script> $.ajax({ url: '{{ url('braintree/token') }}' }).done(function (response) { braintree.setup(response.data.token, 'dropin', { container: 'dropin-container', onReady: function () { $('#payment-button').removeClass('hidden'); } }); }); </script> @endsection 

В приведенном выше блоке кода мы делаем запрос к URL-адресу braintree/token , чтобы получить ответ JSON, содержащий токен. Этот токен мы CLIENT-TOKEN-VALUE . Как только форма оплаты загружается, мы удаляем скрытый класс с нашей кнопки Pay Now , чтобы сделать его видимым для наших клиентов.

Имея это в виду, мы можем попытаться перезагрузить страницу и увидеть форму оплаты, которая выглядит следующим образом:

Форма оплаты

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

приложение / ресурсы / виды / планы / show.blade.php

 [...] <form action="{{ url('/subscribe') }}" method="post"> <div id="dropin-container"></div> <input type="hidden" name="plan" value="{{ $plan->id }}"> {{ csrf_field() }} <hr> <button id="payment-button" class="btn btn-primary btn-flat hidden" type="submit">Pay now</button> </form> [...] 

Форма отправит запрос POST к URL-адресу /subscribe — нам еще предстоит создать маршрут и действие контроллера для его обработки. Мы также добавили скрытый ввод в нашу форму. Это поможет SubscriptionsController узнать план, на который мы подписываемся. Затем, чтобы защитить пользователей от подделки межсайтовых запросов при отправке формы, мы использовали хелпер Laravel csrf_field для генерации токена CSRF.

Давайте создадим маршрут и контроллер, отвечающий за подписку пользователей:

 Route::group(['middleware' => 'auth'], function () { [...] Route::post('/subscribe', 'SubscriptionsController@store'); }); 
 php artisan make:controller SubscriptionsController 

Внутри контроллера добавьте действие store . Это действие отвечает за создание и добавление новых подписок в базу данных:

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

 [...] use App\Plan; [...] class SubscriptionController extends Controller { public function store(Request $request) { // get the plan after submitting the form $plan = Plan::findOrFail($request->plan); // subscribe the user $request->user()->newSubscription('main', $plan->braintree_plan)->create($request->payment_method_nonce); // redirect to home after a successful subscription return redirect('home'); } } 

Что мы делаем внутри метода, так это получаем план из значения, которое мы передали в скрытом вводе. Как только мы получим план, мы вызываем метод newSubscription для текущего зарегистрированного пользователя. Этот метод пришел с чертой Billable которая требовалась в модели User . Первым аргументом, передаваемым методу newSubscription должно быть имя подписки. Для этого приложения мы предлагаем только ежемесячные подписки и, таким образом, называем нашу main подписку. Второй аргумент — это конкретный план Брейнтри, на который подписывается пользователь.

Метод create принимает в качестве аргумента payment_method_nonce который мы генерируем из Braintree. Это метод create , который начинает подписку, а также обновляет нашу базу данных с помощью идентификатора клиента и другой соответствующей платежной информации.

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

Теперь мы можем подтвердить, что все работает, заполнив форму тестовой картой, а затем перейдя на панель инструментов Braintree, чтобы узнать, была ли карта выставлена. Для номера карты используйте 4242 4242 4242 4242 и установите дату на будущую дату. Что-то вроде этого:

Заполнение формы

Если все прошло, как ожидалось, новый платеж должен быть отражен в вашей панели инструментов braintree. Новая запись также будет добавлена ​​в таблицу подписок.

Вывод

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

В следующей части мы, помимо прочего, запретим пользователям дважды подписываться на один и тот же план. Будьте на связи.