Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Spatie Price Api Laravel Package

spatie/spatie-price-api

A small Laravel package that retrieves product prices from the spatie.be API, used on promotional sites for Spatie's own products. Open source but not intended for third-party use.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require spatie/spatie-price-api

First thing to do: find the real class. The README points at App\Support\SpatiePrices\SpatiePriceApi, which is almost certainly a leftover from Spatie's own app namespace rather than the package's shipped namespace. Confirm before you write a single line of integration code:

composer show spatie/spatie-price-api
grep -rn "class SpatiePriceApi" vendor/spatie/spatie-price-api/src

Then open vendor/spatie/spatie-price-api/src/ and read the class end-to-end — it is small enough to read in one sitting, and you need to see the actual method signature of getPriceForPurchasable() (what argument type it accepts, what DTO it returns, which fields the price object exposes). There is no config file, no service provider options, and no documented response schema, so the source is the documentation.

First use case — prove you can get a number out of it. Do this in php artisan tinker before touching Blade:

$purchasable = \App\Models\Product::first(); // or whatever your model is
$price = \SpatiePriceApi::getPriceForPurchasable($purchasable);

dump($price); // inspect the actual shape: amount, currency, discount fields

Once you have a number in a REPL, the two surfaces you will actually use day-to-day are:

  1. Server-side: getPriceForPurchasable() inside a controller, view composer, or Livewire component.
  2. Client-side: SpatiePriceApi::scripts() rendered once per page, which registers the Alpine components spatiePrice(id) and spatieBundlePrice(id) plus the discount countdown.

Before you go further, read the README's own disclaimer. It says the package is not intended for third-party use, the endpoint is spatie.be's, and Packagist shows zero dependents. That does not stop you evaluating it, but it does mean you should treat this as a reference implementation you may re-implement behind your own interface, not a dependency you commit a revenue-bearing page to.

Implementation Patterns

Pattern 1 — cache the server-side call, always. The call is synchronous and goes over the network on the render path. Never let a page view hit the upstream API directly.

use Illuminate\Support\Facades\Cache;

$price = Cache::remember(
    "price:{$purchasable->getKey()}",
    now()->addMinutes(15),
    fn () => SpatiePriceApi::getPriceForPurchasable($purchasable)
);

Pick a TTL short enough that a genuine price change propagates within a marketing window, and long enough that a traffic spike on a promo page does not become a traffic spike to spatie.be.

Pattern 2 — wrap it behind your own interface on day one. The package has no provider abstraction, so if you call the static method directly in ten Blade templates you have hard-coded the vendor into your view layer.

interface PriceProvider
{
    public function forPurchasable(Product $product): Price;
}

final class SpatiePriceProvider implements PriceProvider
{
    public function forPurchasable(Product $product): Price
    {
        $raw = Cache::remember(/* ... */ fn () => SpatiePriceApi::getPriceForPurchasable($product));

        return Price::fromApiResponse($raw);
    }
}

Bind it in a service provider and inject PriceProvider everywhere. This costs you twenty minutes now and is the difference between a swap later being a one-class change versus a codebase-wide grep.

Pattern 3 — server-render the price, enhance with Alpine. Client-side fetch means the price is absent from initial HTML, which breaks SEO, listing pages, and no-JS visitors. Render the base price in Blade and let the Alpine component handle regional/temporal updates:

{{-- once, near the end of body --}}
{!! SpatiePriceApi::scripts() !!}

{{-- the authoritative, prerendered value --}}
<span data-price="{{ $price->formatted }}">{{ $price->formatted }}</span>

{{-- the Alpine enhancement, keyed by purchasable id --}}
<div x-data="spatiePrice({{ $product->id }})">
    <span x-text="price"></span>
    <span x-show="discountActive" x-text="countdown"></span>
</div>

The Alpine components are roughly 50 lines each — read them in vendor/ before wiring them up so you know which property names (price, countdown, discountActive, or whatever they actually are) your template must reference.

Pattern 4 — bundles get their own call. Since 1.1.0 there is a bundle price path, surfaced client-side as spatieBundlePrice(id). Treat bundle and unit pricing as two distinct components; do not try to derive one from the other.

<div x-data="spatieBundlePrice({{ $bundle->id }})">
    <span x-text="price"></span>
</div>

Pattern 5 — the countdown is decoration, not a rule. The browser-side countdown is driven by the visitor's clock. Any actual deadline, eligibility check, or discount application must be re-validated server-side at cart and checkout. The pattern is: Alpine shows urgency, the checkout controller enforces truth.

// at checkout, never trust the client
$price = $this->prices->forPurchasable($product); // fresh, uncached or short TTL
if (! $price->discountIsCurrentlyValid()) {
    // recompute, or reject with a clear message
}

Pattern 6 — test with Http::fake(). The static entry point makes constructor injection impossible, so intercept at the HTTP layer instead:

Http::fake([
    'spatie.be/*' => Http::response([/* captured real payload */], 200),
]);

$price = SpatiePriceApi::getPriceForPurchasable($product);
$this->assertSame(1999, $price->amountInCents);

Capture one real response body early and commit it as a fixture — that fixture is your only defense against a silent schema change upstream.

Pattern 7 — pin the version and watch the changelog. Release history is essentially Laravel 10/11/12/13 compatibility bumps plus one Alpine feature in 1.7.0. Pin to ^1.7 and read the changelog on every bump, because a "Laravel 13 support" release is a fine moment to also change the response shape.

Gotchas and Tips

The README's namespace is wrong for consumers. App\Support\SpatiePrices\SpatiePriceApi is Spatie's own app namespace. Verify the real one from vendor/ before pasting anything into your codebase; if the class genuinely ships without a namespace prefix you recognize, alias it.

There is no config to publish. No base URL, no timeout, no retry, no auth token setting. Whatever is hard-coded in the class is what you get. If you need a configurable endpoint or a request timeout, you are editing vendor code or wrapping it — another reason for Pattern 2.

Network flakiness is a known failure mode. Changelog 1.1.2 is literally "Handle FreeGeoIP timeouts," which tells you the package's own history includes upstream timeouts on the render path. Always pair the call with a try/catch and a fallback price:

try {
    $price = $this->provider->forPurchasable($product);
} catch (\Throwable $e) {
    report($e);
    $price = Price::fallback($product); // last-known-good, cached or hard-coded
}

A promo page that renders a stale price beats a promo page that renders nothing.

Client-side fetch bypasses your CDN entirely. Every visitor triggers their own request to spatie.be. On a successful campaign that is un-cacheable egress you do not control, and you have no visibility into rate limiting or throttling on the far side. Server-render + cache, per Pattern 3, is not optional at any real traffic level.

Verify scripts() against your asset pipeline before shipping. It injects JavaScript. If you run Vite with a strict CSP, or you are on Inertia/Vue/React, inline injection will either be blocked or pointless — you will be re-implementing the two Alpine components as a bundled entry anyway. Check your CSP script-src first; the components are short enough that a rewrite is cheap.

The static call is untestable by injection. You cannot swap the implementation in a unit test. Http::fake() is the escape hatch, but it means your test suite is coupled to the upstream URL and payload shape.

No fallback dataset ships with the package. If spatie.be is down, rate-limiting, or has changed its contract, you have no local price store to fall back to. Seed your own last-known-good cache and treat the API as a refresh, not a source of truth.

Absent from the changelog does not mean absent from the payload. The README documents one method and the Alpine components document themselves. Inspect the returned DTO in tinker and write down every field you depend on, with a fixture test per field — that is your only contract.

Extension points are effectively nil. No interface to implement, no driver to register, no events fired, no macros. The only extension point is your own wrapper class, which is exactly why Pattern 2 is the first thing you should build and not the last.

The honest day-to-day read: this is a 100-line HTTP client plus two Alpine components. If your goal is to ship promotional pricing, budget half a day reading it as a reference and two days building your own version against a pricing source you control, behind an interface, with a cache, a fallback, and a server-rendered price in initial HTML. Adopting the dependency directly means pinning a revenue-visible value to an undocumented, unversioned endpoint owned by a party who has stated the package is not for you.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route