skip to content

Official Add-Ons

Laravel's first-party packages for product features: Scout full-text search, Cashier Stripe billing and Pennant feature flags. Interviewers ask when each beats a hand-rolled build.

on this pageshow

explore

questions

15

What is Laravel Cashier (Stripe) for, and what does adding the Billable trait to the User model give you?

level: juniorimportance: must knowfreq 38%

answer

  1. a Stripe billing wrapper, not a gateway
  2. published migrations add stripe_id and trial_ends_at
  3. subscriptions and subscription_items tables
  4. subscribed() checks the default type
  5. invoices() calls the Stripe API

basics

~20 s

Laravel Cashier wraps Stripe's billing API for an Eloquent model. The Billable trait adds customer columns, a subscriptions relation and methods such as newSubscription(), subscribed(), checkout(), charge() and invoices(), while Stripe remains the system that actually bills customers.

solid answer

~30 s

Cashier (`laravel/cashier`, version 16 for Stripe) is a first-party layer over Stripe Billing: Stripe stores prices, charges cards and runs renewals, and Cashier mirrors the result into your database and gives you an expressive API. You publish its migrations (`vendor:publish --tag="cashier-migrations"`), which add `stripe_id`, `pm_type`, `pm_last_four` and `trial_ends_at` to `users` and create `subscriptions` and `subscription_items`. Adding `Laravel\Cashier\Billable` to `User` composes traits for customers, payment methods, subscriptions, charges, invoices and taxes, so you get `newSubscription()`, `subscribed()`, `checkout()`, `charge()`, `invoices()` and `downloadInvoice()`. Local rows are a cache of Stripe's state; invoices are read from Stripe on demand.

code

php · 11 lines
php
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Cashier\Billable;

class User extends Authenticatable
{
    use Billable;
}

go deeper

for a junior

Recall that Cashier is Laravel's wrapper for Stripe billing, that Billable goes on the User model, and that subscribed() is the everyday check for a paying customer.

for a middle

Explain the migrations and tables, the traits Billable composes, what subscribed() treats as valid, and why invoices() is a live Stripe call.

for a senior

Treat the local tables as a webhook-fed cache of Stripe, avoid Stripe calls on hot paths, and protect invoice downloads by customer ownership.

for a principal

Judge whether Cashier's Stripe-shaped model fits the product's pricing, and what it would cost to move billing providers once subscriptions depend on it.

## What Cashier is, and what it is not **Laravel Cashier (Stripe)** is a first-party package, `laravel/cashier`, currently at version 16, that puts an Eloquent-friendly API over **Stripe Billing**. It is not a payment gateway and it does not hold card data: - **Stripe** owns products, prices, customers, payment methods, invoices and the renewal schedule, and it charges the card. - **Cashier** creates and updates those Stripe objects through the Stripe SDK, stores a small local copy of subscription state, and keeps that copy current through webhooks. For a podcast-hosting SaaS with a monthly and a yearly plan, you define both prices in the Stripe dashboard and refer to them by price ID, such as `price_podcast_monthly`, from Laravel. ## Installing it 1. `composer require laravel/cashier`; 2. `php artisan vendor:publish --tag="cashier-migrations"` and `php artisan migrate`, because the migrations are published into your app rather than loaded from the package; 3. set `STRIPE_KEY` and `STRIPE_SECRET`, plus `STRIPE_WEBHOOK_SECRET` for signed webhooks; 4. add the `Billable` trait to the model that pays, which is `App\Models\User` by default. The migrations create this local schema: | Table | Columns that matter | |---|---| | `users` (added columns) | `stripe_id`, `pm_type`, `pm_last_four`, `trial_ends_at` | | `subscriptions` | `type`, `stripe_id`, `stripe_status`, `stripe_price`, `quantity`, `trial_ends_at`, `ends_at` | | `subscription_items` | one row per price on a subscription | ## What the Billable trait composes `Billable` is a bundle of smaller traits, which is why one `use` line gives a model so many methods: - **`ManagesCustomer`**: `createOrGetStripeCustomer()`, `stripeId()`, `redirectToBillingPortal()`, balances and tax IDs; - **`ManagesSubscriptions`**: `newSubscription()`, `subscription('default')`, `subscribed()`, `onTrial()`, `subscribedToPrice()`; - **`ManagesPaymentMethods`**: default and stored payment methods; - **`PerformsCharges`**: `charge()`, `pay()`, `refund()`, and `checkout()` for one-off Checkout sessions; - **`ManagesInvoices`**: `invoices()`, `findInvoice()`, `downloadInvoice()`, `upcomingInvoice()`; - **`HandlesTaxes`** and **`ManagesUsageBilling`** for tax rates and metered prices. ## Checking access `subscribed()` takes a subscription **type**, `'default'` unless you name one, and returns true when that subscription is **valid**: active, on trial, or cancelled but still inside its grace period. It reads the local `subscriptions` rows, so it costs no Stripe call. Gating the podcast upload page is usually `abort_unless($request->user()->subscribed(), 402)` or a middleware that does the same. ## Invoices are read from Stripe Cashier keeps **no invoices table**. `$user->invoices()` calls the Stripe API each time, returning up to 24 paid invoices by default as `Laravel\Cashier\Invoice` objects; `invoicesIncludingPending()` adds open ones. `downloadInvoice($id)` renders a PDF, which needs `dompdf/dompdf` installed for the default renderer, and it refuses an invoice that belongs to another Stripe customer with a **403**, or an unknown ID with a **404**. Because every listing is a remote call, cache or paginate it on busy account pages. ## Interview traps - **"Cashier stores the card."** It stores only `pm_type` and `pm_last_four` for display; Stripe holds the payment method. - **"`subscribed()` means paid this month."** It is also true during a trial and during the grace period after `cancel()`. - **"The local tables are the truth."** They mirror Stripe, and webhooks keep them current; state changed in Stripe, such as a failed renewal, only reaches your tables through a webhook. ## Versions worth knowing Cashier for Stripe is at 16.8 on this pin and supports Laravel 13. Two older changes still shape answers: since Cashier 14, `dompdf/dompdf` is an optional dependency you install yourself for invoice PDFs, and new subscriptions default to Stripe's `default_incomplete` payment behaviour. Cashier also pins the Stripe API version it speaks: its `STRIPE_VERSION` constant reads `Stripe\Util\ApiVersion::CURRENT` from the installed Stripe SDK, so the exact version follows the SDK release, and the `cashier:webhook` command creates endpoints on that version so payload shapes match. A separate package, Cashier Paddle, targets Paddle instead of Stripe; its API is similar in spirit but not identical, so name which one you mean in an interview.

  • In Laravel Cashier, what is the difference between $user->onTrial() and $user->onTrial('default')?
    With no arguments, `onTrial()` first checks the user's own `trial_ends_at`, a generic trial with no Stripe subscription yet, and then the default subscription's trial. Passing any argument, even `'default'`, skips the generic check and only looks at that subscription's `trial_ends_at`.
  • Why does Laravel Cashier publish its migrations instead of loading them from the package?
    Publishing copies them into `database/migrations`, so they become part of your app's history: you can change the billable table or column types before migrating, and the schema does not shift when the package updates. You run `vendor:publish --tag="cashier-migrations"` once, then `migrate`.

saying these in an interview costs you the question

  • Cashier processes card payments itself, so Stripe is optional.
  • Cashier stores full card details in the users table.
  • $user->invoices() reads from a local invoices table.
  • subscribed() returns false while a customer is on a trial.
  • The Billable trait works without running Cashier's migrations.
open as a page

In Laravel Pennant, how do you define a feature with Feature::define(), check it with Feature::active(), and what scope is used by default?

level: juniorimportance: must knowfreq 30%

basics

~10 s

Feature::define('new-checkout', fn (User $user) => ...) registers a resolver, usually in AppServiceProvider::boot(). Feature::active('new-checkout') checks it for the default scope, the authenticated user, and Feature::for($scope) checks another user, team or value.

open as a page

In Laravel Scout, what does adding the Searchable trait to an Eloquent model do, and how does its search index stay in sync?

level: juniorimportance: must knowfreq 42%

basics

~20 s

The Searchable trait registers a Scout model observer that upserts the record into the search index on every Eloquent save and removes it on delete, using toSearchableArray() as the document and searchableAs() as the index name.

open as a page

In Laravel Cashier, how does newSubscription(...)->checkout() start a subscription, and when does the local subscriptions row appear?

level: middleimportance: should knowfreq 30%

basics

~20 s

newSubscription('default', $priceId) returns a SubscriptionBuilder whose checkout() creates a Stripe Checkout session and redirects there. The local subscriptions row is written later, when Cashier's webhook handler receives customer.subscription.created, not when the customer returns to success_url.

open as a page

In Laravel Cashier, how do swap(), cancel(), cancelNow() and resume() change a subscription, and what do onTrial() and onGracePeriod() report?

level: middleimportance: should knowfreq 32%

basics

~20 s

swap() moves to new prices, prorating by default. cancel() sets ends_at to the period end, leaving a grace period; cancelNow() ends it at once; resume() works only inside the grace period. onTrial() and onGracePeriod() compare trial_ends_at and ends_at with now.

open as a page

With Laravel Pennant, how does a Lottery-based 10% rollout stay stable per user, and how do the database and array drivers change that?

level: middleimportance: should knowfreq 26%

basics

~20 s

Pennant draws the lottery on a user's first check and stores the result in the features table by feature name and scope, so later checks reuse it. The array driver keeps results in memory only, so each request draws again.

open as a page

In Laravel Pennant, how do activate(), deactivate(), activateForEveryone() and purge() change stored flag values, and when would you run pennant:purge?

level: middleimportance: should knowfreq 18%

basics

~20 s

activate() and deactivate() store a value for one scope, forget() deletes it, and activateForEveryone() overwrites every stored row for a feature. purge() deletes a feature's rows so the next check re-runs its definition; pennant:purge does that from a deploy.

open as a page

In Laravel Scout 11, how do the database and collection engines differ from Algolia, Meilisearch and Typesense, and when would you choose each?

level: middleimportance: should knowfreq 30%

basics

~20 s

The database and collection engines search your own tables with no separate index; Algolia, Meilisearch and Typesense keep an external index Scout must sync. Collection suits tiny datasets, database suits MySQL or PostgreSQL, external engines add typo tolerance and facets.

open as a page

In a Laravel Cashier app, why are Stripe webhooks the source of truth for subscription state, and how do you set up /stripe/webhook safely?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Renewals, failed payments, portal cancellations and Checkout completions all happen inside Stripe, so only webhooks tell Cashier about them. Register /stripe/webhook with cashier:webhook, set STRIPE_WEBHOOK_SECRET so signatures are verified, and exempt the path from CSRF.

open as a page

Checking a Laravel Pennant flag inside a loop over 500 users fires hundreds of queries and returns false in queued jobs. Why, and how do you fix both?

level: seniorimportance: should knowfreq 20%

basics

~20 s

Each Feature::for($user)->active() looks up one scope, so a loop queries once per user; Feature::for($users)->loadMissing() fetches them in bulk first. Queued jobs have no authenticated user, so the null default scope makes a User-typed resolver resolve false.

open as a page

In Laravel Scout, why can a mass update such as Recipe::where(...)->update([...]) leave the search index stale, and how do you repair it?

level: seniorimportance: should knowfreq 24%

basics

~20 s

A query-builder update runs one SQL statement without loading models, so no Eloquent events fire and Scout's observer never runs. Repair it by chaining searchable() onto the same query, or with scout:import, adding --fresh to clear orphans.

open as a page

In Laravel Scout, what changes when SCOUT_QUEUE is true, and why would you also turn on the after_commit option?

level: seniorimportance: should knowfreq 28%

basics

~10 s

With SCOUT_QUEUE true, Scout dispatches MakeSearchable and RemoveFromSearch jobs instead of calling the engine during the request. after_commit delays syncing until open transactions commit, so rolled-back or uncommitted rows never reach the index.

open as a page

In a Laravel app using Pennant, how do the @feature Blade directive and the EnsureFeaturesAreActive middleware gate UI and routes, and what do they return?

level: middleimportance: nice to knowfreq 16%

basics

~20 s

@feature('new-checkout') renders its block when the flag is active for the default scope, and @feature('name', 'value') compares a rich value. EnsureFeaturesAreActive::using('new-checkout') on a route aborts with 400 when any listed feature is inactive, unless whenInactive() sets another response.

open as a page

In Laravel Scout 11, how do search()->where() and paginate() behave differently from the same calls on an Eloquent query builder?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

Scout's where() filters documents inside the search engine, so it only sees fields from toSearchableArray() and supports field comparisons, whereIn and whereNotIn, not closures or orWhere. paginate() returns a LengthAwarePaginator whose total comes from the engine.

open as a page

In Laravel Cashier, what does an IncompletePayment exception mean during a subscription or charge, and how do you handle SCA confirmation?

level: seniorimportance: nice to knowfreq 20%

basics

~20 s

IncompletePayment means Stripe could not finish the payment without the customer, usually a 3D Secure step required by Strong Customer Authentication. Catch it, redirect to Cashier's cashier.payment route with the payment ID, and treat the subscription as incomplete until confirmed.

open as a page