vanilo/contracts
vanilo/contracts defines the shared PHP interfaces behind Vanilo’s e-commerce components, providing consistent contracts for products, orders, customers, pricing, and related services so Laravel packages can interoperate cleanly.
vanilo/contracts is pulled in transitively by vanilo/framework, vanilo/product, vanilo/order, vanilo/cart, etc. Run composer why vanilo/contracts to see who depends on it and which version constraint is in play before you touch anything.composer require vanilo/contracts) only when you are building your own integration package — a payment gateway, a warehouse sync, a product importer — that must talk to Vanilo without dragging in the whole framework, or when you want to unit-test against the domain interfaces without a database.vendor/vanilo/contracts/src/. It is small — one interface per file — and reading the whole directory once gives you Vanilo's domain map: Product, Buyable, Priceable, Taxon, Cart, Checkout, Order, Customer, Address, Billpayer, PaymentRequest/PaymentResponse, shipping/shippable contracts.App\Models\Product for Vanilo\Contracts\Product in a constructor, and the same service now works with stock models, your custom models, and test doubles:namespace App\Services;
use Vanilo\Contracts\Product;
class PricingService
{
public function __construct(private Product $product) {}
public function effectivePrice(): float
{
return $this->product->getPrice() * (1 - $this->discount());
}
}
If that resolves from the container without a manual binding, the package's service provider already maps the interface; if it throws, see the gotchas below.
Interface-typed services (the 80% case). Inject contracts at the edges of your code and keep concrete models at the persistence boundary. Business logic (pricing, eligibility, checkout orchestration) depends only on the contract; Eloquent stays in repositories and controllers.
Custom models that implement the contracts. When you replace a Vanilo model with your own (a Course or Subscription that is a "product"), implement the relevant interfaces on your model and point the package's model mapping at it — the models entry in the package config or the model registration in your module service provider. The contract is the checklist: every accessor listed must exist and return the declared type.
Adapters for legacy domains. The highest-value pattern when migrating onto Vanilo. Wrap an existing table/model and delegate:
class LegacyProductAdapter implements \Vanilo\Contracts\Product
{
public function __construct(private \App\Legacy\Item $item) {}
public function getName(): string { return $this->item->title; }
public function getPrice(): float { return (float) $this->item->price_cents / 100; }
// ...remaining contract methods delegate to $this->item
}
Now your new checkout code consumes legacy data without a big-bang migration.
Plugging in a payment provider. Implement the payment request/response contracts (plus the gateway contract your Vanilo version exposes) and register the gateway. Because these are value-like objects, make them serializable if you persist or queue them.
Custom shipping / carriers. Same shape: implement the shipping-method contract, return a cost for a given shippable/address, and register it with the shipping subsystem.
Sub-interfacing for your own domain. When you need methods the core doesn't declare, define interface SellableProduct extends \Vanilo\Contracts\Product { public function subscriptionLength(): int; } and implement both. Extend, don't fork — core packages keep accepting the parent type.
Testing. The contracts are plain interfaces, so they are trivial to mock with Mockery or anonymous classes. This is the cheapest way to unit-test a checkout flow: fake Cart, Billpayer, and PaymentResponse, assert your orchestration, no database.
Static analysis. Add PHPStan/Larastan and type-hint contracts in your service layer. The analyzer will then catch places where you leaked an Eloquent-only method (->where(...), ->save()) into code that is supposed to be implementation-agnostic.
new anything from this package; it is a vocabulary, not an implementation. If you expected helper methods, they are in the concrete packages.vanilo/contracts v3 against vanilo/order v2 produces fatal signature mismatches that look like unrelated bugs. Pin the whole Vanilo stack together and check the changelog on upgrade — even a minor addition to an interface is breaking for anyone who implements it.Target [Vanilo\Contracts\X] is not instantiable means the interface was never bound to a concrete class. Before assuming a container bug, check whether the package resolves these from a model map rather than a binding — app(SomeContract::class) is not guaranteed to work for every contract. Resolve the concrete model and trust instanceof instead of injecting the interface blindly."19.99" strings. A contract declaring float will throw a TypeError at runtime. Add $casts on the implementing model and test with real (non-sqlite) data at least once.null explicitly; don't let a ?Address reach a method expecting Address.Model&\Vanilo\Contracts\Product is usually a sign the abstraction isn't buying you anything. Pick the boundary: contracts for logic, models for persistence.getItems() may trigger a relation load. Calling it in a loop over 500 orders is an N+1. Eager-load in the repository before handing objects across the contract boundary.instanceof guards get noisy. Scattering if ($x instanceof Product) through your code defeats the point. Prefer a single adapter at the edge over defensive checks everywhere.illuminate/contracts either. The real signal is the consuming packages — read their composer.json for the supported contracts range. The opportunity here is contribution: adding a well-justified method to an interface (with adapters in the core packages) is a small change with wide reach.How can I help you explore Laravel packages today?