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