dashed/dashed-ecommerce-core
Dashed Ecommerce Core is a Laravel/PHP foundation for building online stores, offering core commerce models, cart and checkout logic, and extensible services for products, orders, payments, and catalog management.
Install and publish in one pass, then read the service provider before you write a single line of application code.
composer require dashed/dashed-ecommerce-core
php artisan vendor:publish --provider="Dashed\Ecommerce\EcommerceServiceProvider"
php artisan migrate
Read these four things in this order — they tell you 90% of what the package does:
src/*ServiceProvider.php). Every bind()/singleton() is a seam you can swap. Note the config keys it merges and the migrations it loads — that's your override surface.config/dashed-ecommerce.php or similar). Look for arrays of model class-strings (product, variant, order, cart) and money/currency settings. Model-swapping via config is the standard convention for this category of package; if it's there, it's the primary extension mechanism.database/migrations/. Read the foreign keys — they dictate the order in which you can safely seed data and which tables you may extend.src/Contracts/. If contracts exist, the container bindings in the provider are your swap points. If they don't, you'll be extending concrete models instead, which changes your upgrade strategy.First use case to validate the install: create a product with at least one variant and a price, resolve the cart service from the container, add the variant, and place an order. Do this in a tinker session or a throwaway test before touching real application code — it exercises the money handling, the variant/pricing resolution, and the order lifecycle in one shot, and any of those three failing is a signal to stop and read further.
$product = Product::create([...]);
$variant = $product->variants()->create([...]);
$cart = app(Cart::class); // or whatever the contract resolves to
$cart->add($variant, 1);
$order = $cart->toOrder();
If that sequence is awkward — if you find yourself reaching for DB::table() or manually setting foreign keys — the package's own conventions are probably different from what you assumed, and it's worth rereading the provider before building on top of it.
Configure, don't fork. For a package with this little downstream usage, your biggest risk is patching vendor code that you then can't update. Push every customization through config, container bindings, and events. If a requirement genuinely can't be met that way, write an adapter class in your own namespace that wraps the package and inject that — it gives you a single file to rewrite if you ever have to replace the dependency.
Model swapping is usually the intended integration path. E-commerce cores typically let you point config at your own subclasses so you can add relations, scopes, and accessors without touching vendor/. The pattern:
// config/dashed-ecommerce.php
'models' => [
'product' => App\Models\Product::class,
'order' => App\Models\Order::class,
],
// app/Models/Product.php
class Product extends \Dashed\Ecommerce\Models\Product
{
public function scopeActive($q) { return $q->where('is_active', true); }
}
Do this before your first migration run. Swapping models after data exists means your polymorphic relations (if any) and any hard-coded class names in the package's own seeders/factories will be inconsistent. If the package uses morph maps, register your subclasses there too.
Treat money as integers, end to end. Most cores in this space store minor units and expose a Money value object or a cast. Never do float arithmetic across the boundary — cast to integer at the edge, do all math in integers, format only at render time. If the package ships a formatting helper, use it rather than number_format, or you'll get currency-symbol and decimal-place drift between the admin and the storefront.
Wire side effects through events, not model observers on vendor classes. Order placement, payment capture, refunds, and stock changes should each fire an event. Register listeners for email, inventory decrement, and analytics. Observers on vendor models work but break silently on package upgrades; event listeners are the contract the package author actually intended. Check whether events fire inside a database transaction — if they do, use DB::afterCommit() or ShouldHandleEventsAfterCommit on listeners that touch external services.
Cart vs. order is a service boundary. Carts are mutable and session/identity-scoped; orders are immutable snapshots. The most common integration bug in this architecture is pricing the order from live product prices instead of copying the line-item price at checkout time. Verify that toOrder() snapshots prices, tax rates, and currency onto the order lines. If it doesn't, add that in your order-creation wrapper — it's non-negotiable for accounting.
Resolve services from the container, never new. Anything with a contract should be app(Contract::class) or constructor-injected. This is what makes the package testable and swappable, and it's how your own tests should consume it.
Test against the package's factories. Use RefreshDatabase and the shipped factories to build realistic fixtures; hand-rolling inserts will drift from the schema on every migration. Write one integration test per lifecycle transition (cart → order, order → paid, order → refunded) so package upgrades fail loudly in CI rather than in production.
"dashed/dashed-ecommerce-core": "1.2.3" (exact) rather than ^1.2, and upgrade deliberately with the integration tests above as your gate.vendor:publish is destructive on re-publish. If you publish the config and later upgrade, Laravel will overwrite it or leave it stale depending on whether you pass --force. Keep published config under version control and diff it against the vendor copy after every upgrade. Migrations are worse: once published, they're yours, and the package's subsequent migration changes won't reach you. Prefer not publishing migrations unless you must modify them, and if you do, note the original filename in a comment so you can spot divergence.vendor/, your migration timestamps must still sort later. A php artisan migrate:fresh --seed on a clean DB is the only reliable check.with(['variants.prices'])) on every index query; the package usually won't do it for you, and the default $with on the models is often empty by design.php artisan about to confirm the provider registered, php artisan vendor:publish --list to see available tags, app()->bound(SomeContract::class) in tinker to confirm bindings, and DB::listen() in a local middleware to see exactly which queries a cart operation emits. When behavior surprises you, the resolved class is usually not the one you assumed — get_class(app(Cart::class)) settles it in one line.NOTES.md of every seam you've used — it turns a future package swap from an archaeology project into an afternoon.How can I help you explore Laravel packages today?