unisharp/buyable
UniSharp Buyable is a Laravel package for adding purchase functionality to Eloquent models. It offers a lightweight way to mark items as buyable and build simple cart, checkout, or order workflows without reinventing core ecommerce logic.
Install the package
composer require unisharp/buyable
There is no service provider to publish and no config file to touch — the package is composer + trait only. If you are on Laravel 5.5+ the auto-discovery will pick up anything it registers; on older versions check composer.json for a providers array before assuming zero setup.
Read the source before writing code. With three stars and a last release in March 2018, the README is not the source of truth — the package is. Open these first, in this order:
src/Contracts/ — this is the actual API surface. Everything you integrate against is a contract here (a buyable contract exposing price/currency, possibly a pricing-strategy contract).src/Traits/ — the trait you will use inside your Eloquent model. Confirm the exact method names it expects you to implement versus the ones it provides.composer.json — check the illuminate/* constraints. This tells you immediately whether the package is viable on your Laravel version.First use case: make one model buyable.
use Illuminate\Database\Eloquent\Model;
use UniSharp\Buyable\Traits\Buyable; // verify exact namespace in src/Traits
class Plan extends Model
{
use Buyable;
// implement whatever the contract requires — typically price + currency
}
Then assert the contract in a test rather than trusting the trait:
$this->assertInstanceOf(\UniSharp\Buyable\Contracts\Buyable::class, new Plan);
That single test is your smoke test for the whole integration — if the trait and contract disagree, you find out here and not inside a checkout flow.
Contract-first, trait-second. The value of this package is the contract, not the trait. Type-hint against the contract in your cart/checkout services (Buyable $item) and let the trait be an implementation detail on the model. That way a Plan, a Product, and a Service all flow through the same checkout code, and you can swap the trait for a hand-rolled implementation later without touching consumers.
Keep pricing out of the model. Store price_cents (integer) and currency (ISO 4217 string) as columns, and let the model's accessor do the conversion. Do not compute tax, discounts, or shipping inside the buyable accessor — those belong to the cart/order layer that consumes the contract. The package deliberately stops at "what does this cost, in what currency, and can it be bought."
Swap pricing strategies via the container. The contract-driven design is the extension point: bind your pricing implementation in a service provider and resolve it where you need it. This is how you do per-tenant pricing, tiered/B2B pricing, or seasonal price lists without branching inside the model:
// AppServiceProvider::register()
$this->app->bind(PricingStrategy::class, TenantPricingStrategy::class);
Multi-currency. Treat currency as data, not configuration. If you only ever sell in one currency, still store the column and return it from the accessor — retrofitting currency later means a migration plus an audit of every price comparison in the codebase. If you sell in several, do the conversion at the boundary (cart line creation) and store the resolved amount + currency on the order line, never re-deriving it from the product at display time.
Integration with cart/order packages. The intended workflow is: cart package asks the buyable for price/currency → cart stores a snapshot of that price on the line item → order freezes it permanently. Because this package has no cart or order opinions, you own the snapshot. Make the line item table store unit_price and currency independently of the product row; a later price change on the product must not mutate historical orders.
Testing. Since the surface is tiny, test at two levels:
Use RefreshDatabase and factories; nothing in this package needs mocking.
It is effectively unmaintained. Last release 2018-03-21, three stars, zero dependents. The illuminate/* constraint will almost certainly not allow Laravel 9/10/11. Options, in order of preference: (a) skip it and copy the ~one contract + ~one trait into app/Support/ yourself — it is genuinely that small; (b) fork it and widen the constraints; (c) composer require it with a replace/path repository. Do not assume it installs cleanly on a modern app.
Verify method names before committing to them. The description advertises "price" and "currency" accessors, but whether these are Eloquent accessors (getPriceAttribute), contract methods (price()), or both is only knowable from source. Getting this wrong produces confusing "Call to undefined method" errors deep in a checkout path. Write the contract assertion test from Getting Started immediately.
Money as float is the classic trap. If the accessor returns a float, do not do arithmetic on it. Convert to integer minor units at the earliest boundary ((int) round($price * 100)) and keep everything downstream in integers. If your product table stores decimal(10,2), Eloquent will hand you a string — cast deliberately rather than relying on PHP's coercion.
Trait method collisions. If your model already defines price() or uses another package's pricing trait, PHP will silently prefer the class method over the trait method (or fatal on two traits). Check class_uses_recursive() when behaviour looks wrong, and alias conflicting trait methods (use Buyable { price as buyablePrice; }).
No purchasability state means you implement it. "Purchasability" in the description is not a guarantee that a isPurchasable() gate exists. Model it explicitly — an active boolean, a published_at, a stock check — and enforce it in the cart's add-item path, not in the accessor. Accessors that return null for unavailable items hide bugs; a thrown exception or a boolean guard is debuggable.
Debugging tips.
php artisan tinker → (new Product)->price is the fastest way to confirm the accessor resolves and what type it returns.dump(class_uses_recursive(Product::class)) confirms the trait is actually applied.app()->getBindings() shows what is in the container.Extension points worth knowing. The contract (swappable via container binding) and the trait (overridable per model) are the two seams. Prefer the container binding for pricing strategy and the model method override for one-off exceptions; avoid editing the vendor directory, since you will need to re-apply the change on any fork or reinstall.
Budget for replacement. Given the age and adoption, treat this as a small piece of scaffolding you may absorb into your own codebase. The migration cost is low if you have consistently type-hinted against the contract — which is the strongest argument for doing that from day one.
How can I help you explore Laravel packages today?