NOTAS WEB
Como integrar Stripe con Laravel 9
Stripe es una poderosa pasarela de pago que permite ser integrada de forma muy sencilla en tú web. Permitiendo procesar pagos únicos, suscripciones a productos, generación de facturas, pagos en distintas monedas, etc. Todo eso integrado con un framework tan poderoso como Laravel, te permite crear aplicaciones robustas, fiables y sencillas.
¿Qué es Stripe?
Stripe es un sistema de pago muy parecido a Paypal, por la que se pueden procesar pagos y suscripciones a
través
de internet. Todo eso se integra a través de una poderosa API que se puede implementar prácticamente en
cualquier aplicación o plataforma digital. Algunas de las ventajas principales de Stripe:
- Fácil implementación acompañada de una gran documentación de su API.
- El usuario que pague no debe tener cuenta en Stripe (como si sucede con Paypal)
- Tú no tendrás que guardas ningún dato sensible respecto a número de tarjeta, etc. Stripe se encarga
de eso. - Puedes integrar su propio formulario de pago con javascript y olvidarte de tener crear uno propio.
- La documentación te muestra la forma de
integrar
las peticiones en tú Backend, en distintas tecnologías (PHP, Node, Java, Rubi, Go, .NET, etc)
Pero para ver lo fácil que es de implementar, vamos a crear un proyecto sencillo
que replique una web de venta de libros. Podremos realizar pagos únicos y pagos por medio de suscripción
(mensual / anual) para el
acceso a todos los libros. Para ese proceso de compra integraremos un sistema de pago que obviamente
será
Stripe.
Empezaremos con los comandos necesarios para crear nuestro proyecto. Las versiones en la que los
construiremos la aplicación
será en la versión 9.1 de Laravel y 8.0.2 de PHP.
- Crear proyecto Laravel (composer create-project laravel/laravel
laravelwithstripe) - Laravel-cashier (composer require laravel/cashier)
Con este último comando, instalaremos una dependencia que nos facilitara la
conexión e iteración con la API de Stripe. Pero te recomiendo que te leas la documentación
de laravel/cashier y veas
todos lo que se puede hacer con ella. Desde saber si un usuario tiene
la suscripción activa a poder generar facturas de los pagos realizados.
El siguiente paso, será que te crees una cuenta en Stripe si todavía no la tienes. Además la creación de
la cuenta es gratuita.
Una vez que estés registrado en Stripe, accede al dashboard de desarrollador y veras dos claves que
necesitaras
para conectar tú aplicación con la API de Stripe.
Pero ya que estamos aquí, vamos aprovechar el viaje. Crearemos dos productos. Un plan de suscripción
mensual y otro plan de
suscripción anual.
Para eso accederemos a la sección producto que se encuentra dentro
del dashboard, y pincharemos en «Añadir producto».
Tiene varios campos a rellenar. En nuestro caso lo registraremos así:
Guardaremos el producto. Al crear un producto, se crea una Id necesario más adelante. Lo podremos ver si
pinchamos en el producto
que aparecerá en la lista de la vista producto. En la sección TARIFA, hay un campo que se llama «ID DE
API»
Este id, lo necesitaremos para añadirlo en nuestro proyecto. Porque cuando un usuario se suscriba a
nuestro plan
(Que hace referencia al producto creado en Stripe), tendremos que pasarle este id. Por último crearemos
el
plan de suscripción Anual. El proceso es el mismo que hemos visto anteriormente. Con esto, terminaría
las acciones necesarias en la web de
Stripe.
Comentarte también otras opciones sobre productos. Stripe te da la posibilidad de poder crear productos
desde la API, o crear un
producto con dos precios distintos. Por ejemplo, podrías crear un mismo producto, pero con un precio en
euros y
otro en dólares.
Ya en nuestro proyecto, añadimos cinco variables en nuestro fichero .env . La URL de la API de
Stripe, la
KEY, el SECRET y los dos Id´s de los productos creados
STRIPE_BASE_URI=https://api.stripe.com STRIPE_KEY=pk_test_51IkU4WAK2uIyILzIxlYAjTrkoMOr STRIPE_SECRET=sk_test_51IkU4WAK2uIyILzI5C7O STRIPE_MENSUAL_PLAN=price_uIyILzIkMM STRIPE_ANUAL_PLAN=price_uIyILzIZm7ZF
Ahora configuraremos el «services.php» que está en la ruta «config/services.php». Crearemos un array
«stripe» y le añadimos 5 campos. Los tres primeros hacen referencia a la URL de la API de Stripe, la
KEY , el SECRET y el último que es una campo tipo array, hace referencia a los id´s de nuestros planes
que guardamos en el fichero .env
A tener en cuenta, las key del array plans, deben coincidir con el valor de la columna stripe_plan, de
la tabla «plans»
'stripe' => [
'base_uri' => env('STRIPE_BASE_URI'),
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_SECRET'),
'class' => App\App\Services\StripeService::class,
'plans' => [
'monthly' => env('STRIPE_MENSUAL_PLAN'),
'yearly' => env('STRIPE_ANUAL_PLAN'),
],
],
Porque creamos esta configuración y no usamos
las variables generadas en el fichero
.env. Porque las variables creadas en el archivo .env no son aptas para usarlas en tiempo de
ejecución.
Migraciones, factories y seeders
Pasemos a crear las migraciones necesarias. Crearemos tres tablas más. Books, Images y Plans. El
código
sería el siguiente:
Migración para Books
return new class extends Migration
{
public function up()
{
Schema::create('books', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->longText('description');
$table->float('price');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('books');
}
};
Tabla images
return new class extends Migration
{
public function up()
{
Schema::create('images', function (Blueprint $table) {
$table->id();
$table->string('url');
$table->unsignedBigInteger('imageable_id');
$table->string('imageable_type');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('images');
}
};
Tabla Plans
return new class extends Migration
{
public function up()
{
Schema::create('plans', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->string('stripe_plan');
$table->float('cost');
$table->text('description')->nullable();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('plans');
}
};
Configuraremos los factories. El único que viene ya creado es el del modelo users.
Empezaremos por el modelo Book
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class BookFactory extends Factory
{
public function definition()
{
return [
'name' => $this->faker->sentence(),
'description' => $this->faker->sentence(),
'price' => random_int(8, 15),
];
}
}
El modelo Image:
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class ImageFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
'url' => 'public/books/fake_book.jpg',
];
}
}
Por último modificaremos el fichero DatabaseSeeder.php, para tener una serie de datos en nuestra base de
datos,
cada vez que ejecutemos las migraciones.
El seeder
<?php
namespace Database\Seeders;
use App\Models\User;
use App\Models\Book;
use App\Models\Image;
use App\Models\Plan;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Storage;
class DatabaseSeeder extends Seeder
{
public function run()
{
User::factory()->create([
'name'=> 'User',
'email'=> 'user@mail.com',
'password'=> \bcrypt('12345678'),
]);
Plan::factory()->create([
'name' => 'Monthly', 'slug' => 'monthly', 'stripe_plan' => 'monthly', 'cost' => 30, 'description' => 'One month access to the entire library'
]);
Plan::factory()->create([
'name' => 'Yearly', 'slug' => 'yearly', 'stripe_plan' => 'yearly', 'cost' => 60, 'description' => 'One-year access to the entire library'
]);
$books = Book::factory(10)->create();
foreach ($books as $book) {
Image::factory(1)->create([
'imageable_id' => $book->id,
'imageable_type' =>'App\Models\Book',
]);
}
}
}
Para correr las migraciones con los seeders o para construirla de nuevo, usaríamos el siguiente
comando:
php artisan migrate:refresh --seed
El código de nuestro fichero de rutas:
<?php
use App\Http\Controllers\BookController;
use App\Http\Controllers\PlanController;
use App\Http\Controllers\PaymentController;
use App\Http\Controllers\SubscriptionController;
use Illuminate\Support\Facades\Route;
Route::get('/', [BookController::class, 'index'])->name('bookstore');
Route::get('/cart/{id}', [PaymentController::class, 'show'])->middleware('auth')->name('purchase');
Route::post('/payments/pay', [PaymentController::class, 'pay'])->middleware('auth')->name('pay');
Route::get('/payments/approval', [PaymentController::class, 'approval'])->middleware('auth')->name('approval');
Route::get('/payments/cancelled', [PaymentController::class, 'cancelled'])->middleware('auth')->name('cancelled');
Route::prefix('subscribe')->middleware('auth')->name('subscribe.')
->group(function(){
Route::get('/', [SubscriptionController::class, 'show'])->name('plans');
Route::post('/', [SubscriptionController::class, 'store'])->name('store');
Route::post('/approval', [SubscriptionController::class, 'approval'])->name('approval');
Route::post('/cancelled', [SubscriptionController::class, 'cancelled'])->name('cancelled');
});
Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth'])->name('dashboard');
require __DIR__.'/auth.php';
Ahora vamos a crear el servicio de Stripe, que nos permitirá interactuar con su API. Creamos una archivo
llamado
StripeService y su ruta será «app\Services\StripeServices.php»
Lo esencial de este archivo:
- En el controlador cargamos las variables del fichero config
- Instanciamos la clase StripeClient que instalamos con la dependecia de Laravel-cashier
- El método «handleSuscription», se encarga de llamar al endpoint de Stripe que primero crea un
cliente de
pago y luego lo asocia a producto. En este caso una suscripción mensual o anual. - Los métodos «createIntent» y «confirmPayment», llevan el flujo para pagos únicos o no recurrentes.
Primero
se debe crear una intención de pago para luego terminar confirmandolo.
<?php
namespace App\Services;
use Illuminate\Http\Request;
use App\Traits\ExternalServices;
class StripeService
{
use ExternalServices;
protected $key;
protected $secret;
protected $baseUri;
protected $stripe;
protected $plans;
public function __construct()
{
$this->baseUri = config('services.stripe.base_uri');
$this->key = config('services.stripe.key');
$this->secret = config('services.stripe.secret');
$this->stripe = new \Stripe\StripeClient(
config('services.stripe.secret')
);
$this->plans = config('services.stripe.plans');
}
public function handleSuscription(Request $request)
{
try{
$customer = $this->createCustomer($request->name, $request->email, $request->payment_method);
$price_select_plan= $this->plans[$request->plan];
$subscription = $this->createSubscription($customer->id, $request->payment_method, $price_select_plan);
}catch(\Exception $exception) {
throw new \Exception($exception->getMessage(), 1);
}
return $subscription;
}
/**
* Create a payment intention
*/
public function createIntent(float $price, string $currency = 'eur', string $paymentMethod )
{
return $this->stripe->paymentIntents->create([
'amount' => $price*100,
'currency' => $currency,
'payment_method_types' => ['card'],
'payment_method' => $paymentMethod,
]);
}
/**
* Confirm an intention to pay
*/
public function confirmPayment($paymentIntentId, $paymentMethod)
{
$this->stripe->paymentIntents->confirm(
$paymentIntentId,
['payment_method' => $paymentMethod]
);
}
/**
* Create a new customer
*/
public function createCustomer(string $name, string $email, string $paymentMethod)
{
return $this->stripe->customers->create([
'name' => $name,
'email' => $email,
'payment_method' => $paymentMethod
]);
}
/**
* Create a new subscription
*/
public function createSubscription( $customerId, $paymentMethod, $priceId)
{
return $this->stripe->subscriptions->create([
'customer' => $customerId,
'items' => [
['price' => $priceId],
],
'default_payment_method' => $paymentMethod
]);
}
}
Controladores
Ahora crearemos los controladores necesarios para trabajar con nuestra vistas.
El comando de ejecución y el código de cada uno sería el siguiente:
Desde BookController, nos cargara el catalogo de libros
php artisan make:controller BookController
<?php
namespace App\Http\Controllers;
use App\Models\Book;
use Illuminate\Http\Request;
class BookController extends Controller
{
public function index()
{
$books = Book::all();
return view('bookstore', compact('books'));
}
}
El controlador PaymentController, se encargara de procesar los pagos únicos.
php artisan make:controller PaymentController
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Services\StripeService;
use App\Models\Book;
use Stripe;
use Session;
class PaymentController extends Controller
{
protected $stripe;
public function __construct(StripeService $stripe)
{
$this->stripe = $stripe;
}
public function show($id)
{
$book = Book::find($id);
return view('Purchase', compact('book'));
}
/**
* Creates an intention to pay and its confirmation
*
*/
public function pay(Request $request)
{
$rules = [
'price' => ['required', 'numeric', 'min:5']
];
$request->validate($rules);
try{
$payment_intent = $this->stripe->createIntent($request->price, 'eur', $request->payment_method );
$this->approval($payment_intent->id, $request->payment_method);
}catch(\Excepction $exception) {
return redirect()->back()->withErrors('No se ha podido completar el pago de su pedido')->withInput();
}
$book = json_decode($request->book, true);
return view('confirmation_page', compact('book'));
}
/**
* create payment confirmation
*
*/
public function approval($payment_intent_id, $payment_method)
{
try{
$this->stripe->confirmPayment($payment_intent_id, $payment_method);
}catch(\Excepction $exception){
return false;
}
}
/**
* Cancel a payment
*/
public function cancelled()
{
return redirect()
->route('bookstore')
->withErrors('Se ha cancelado el pago');
}
}
Y por último, desde el SubscriptionController, gestionaremos las suscripciones.
php artisan make:controller SubscriptionController
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Plan;
use App\Services\StripeService;
class SubscriptionController extends Controller
{
protected $stripe;
public function __construct(StripeService $stripe)
{
$this->stripe = $stripe;
}
public function show()
{
$plans = Plan::all();
return view('plans', compact('plans'));
}
public function store(Request $request)
{
$rules = [
'plan' => ['required', 'exists:plans,slug'],
'name' => ['required'],
'email' => ['required'],
];
$request->validate($rules);
try{
$this->stripe->handleSuscription($request);
}catch(\Exception $exception) {
throw new \Exception($exception->getMessage(), 1);
}
return view('confirmation_page');
}
}
Con todo esto, el resultado final sería el siguiente:
Recuerda que esto es solo una parte de
todo lo que se puede hacer con Stripe y Laravel-cashier. También recordarte que en el repositorio tienes
todo
el código disponible para revisar, bajar o clonar. Para probarlo, solo debes añadir tus
claves
de Stripe, correr las migraciones con el comando que te he indicado más arriba.
Por último si tienes alguna duda o sugerencia. Puedes contactarme
a través del formulario de contacto o por medio de mis perfiles en
redes sociales.
Espero que este articulo te haya sido de utilidad y no dudes en compartirlo si crees que puede ser de
utilidad para otras personas.
feliz código a tod@s!!
Desarrollador de software con más de 7 años de experiencia, especializado en desarrollo web y backend. Con habilidades demostradas en PHP, Laravel, Symfony, y una amplia gama de tecnologías modernas. Apasionado por el diseño y desarrollo de software.