softspring/shop-bundle
Softspring Shop Bundle adds a ready-made e-commerce layer to your PHP application: product catalog, categories, carts, checkout, orders and payment integration, with configurable entities and templates you can extend. A practical starting point for building online stores without reinventing core sho...
First, resolve the stack question. softspring/shop-bundle is a Symfony bundle, not a Laravel package. If you are working in Laravel, composer require softspring/shop-bundle will install, but nothing else works: no service provider, no Eloquent models, no Blade views. Treat this as read-only reference code, not a dependency. The rest of this section assumes you are on Symfony 5.4/6.x with PHP 8.x and Doctrine ORM — otherwise stop and evaluate Lunar or Vanilo for Laravel, or Sylius for Symfony.
Minimal install steps (Symfony):
composer require softspring/shop-bundle
# If a Flex recipe exists, answer yes; otherwise register manually:
# config/bundles.php
# Softspring\ShopBundle\SoftspringShopBundle::class => ['all' => true],
Then inspect what was actually registered before writing a single line of feature code:
php bin/console debug:container --parameters | grep -i shop
php bin/console debug:config softspring_shop # note the exact alias — guess-and-check
php bin/console doctrine:mapping:info | grep -i Softspring
That last command is the most important: it tells you which entities the bundle owns (Product, Cart, Order, line items, and possibly Payment) and therefore what schema you are about to inherit. Run doctrine:schema:update --dump-sql on a throwaway database and read the SQL — do not run the migrate yet.
Where to look first in the source, in this order:
src/DependencyInjection/Configuration.php — the config tree is the de facto documentation for this package. Every knob the maintainer thought worth exposing is here.src/Resources/config/services*.yaml — reveals the service IDs, which ones are public, and which are tagged (e.g. payment providers, pricing calculators). This is where extension points live.src/Entity/ — the domain model. Read Order and Cart state fields carefully; the transitions are usually encoded as constants or an enum.src/Resources/views/ — the Twig template namespace you can override.First use case to prove end-to-end: a throwaway branch that does product → add to cart → place order with a stub payment provider. Do not start with the admin UI, and do not start with real payments. The goal of the first day is to discover whether the bundle's notion of "place an order" is a service call, an event, or a workflow — you cannot plan anything until you know which.
Wire it in a single controller first:
public function addToCart(CartManager $cartManager, Product $product, Request $request): Response
{
$cartManager->addProduct($product, $request->get('quantity', 1));
return $this->redirectToRoute('shop_cart_show');
}
If CartManager (or whatever the actual service ID is) is not injectable, that is your first real signal about how opinionated the bundle is — check services.yaml for public: true versus autowiring gaps.
Inject services, not entities. The bundle's value is in its managers/services (cart mutation, order placement, pricing). Grab those via constructor injection and keep the bundle's entity classes at the edges of your code. If you find yourself calling $order->setStatus(...) from a controller, you have already coupled to internals — wrap it.
Anti-corruption layer from day one. Define your own interfaces for the three things you will definitely want to replace:
interface CartService { public function add(string $productId, int $qty): void; }
interface PricingService { public function total(CartInterface $cart): Money; }
interface OrderPlacer { public function place(CartInterface $cart, PaymentDetails $d): OrderId; }
Adapt the bundle behind these. This is the single highest-leverage pattern with an unmaintained package: when the bundle is abandoned or blocks a Symfony upgrade, you swap the adapter instead of rewriting controllers. It also makes the "fork vs. replace" decision reversible, which matters more than any feature the bundle ships.
Extend entities by Doctrine inheritance, not by editing vendor code. The typical Softspring pattern is a mapped superclass or a base entity you subclass in your own src/Entity/:
#[ORM\Entity]
#[ORM\Table(name: 'app_order')]
class Order extends \Softspring\ShopBundle\Entity\Order
{
#[ORM\Column(type: 'string', length: 32, nullable: true)]
private ?string $internalReference = null;
}
Point the bundle's config at your subclass if it exposes a %shop.model.order.class%-style parameter (check Configuration.php for class keys — most Symfony bundles of this shape do). If it does not expose that parameter, that is a serious extensibility gap and should factor into the fork/reject decision.
Template override pattern. Symfony bundles register Twig paths under @SoftspringShop/. Override by creating templates/bundles/SoftspringShopBundle/... mirroring the vendor path. Copy only what you change; do not fork the whole template tree, or you will be diffing it against upstream forever.
Sibling-bundle assumption. This package very likely expects Softspring's user/store/CMS bundles for authentication and catalog context. Before you write glue, check the composer require section of composer.json in the vendor directory (vendor/softspring/shop-bundle/composer.json) — that tells you the real dependency surface. If it hard-requires siblings, adopting one bundle means adopting the family, and that is a platform bet, not a library choice.
Event/listener integration. Look for dispatched events (OrderPlacedEvent, CartUpdatedEvent) in src/Event/. These are the cleanest integration seam — subscribe in your own src/EventSubscriber/ to hook in analytics, emails, ERP sync, or inventory decrement without touching bundle code. If there are no events, you are back to decorating services.
Decorate rather than override. For behavioural changes (tax calculation, shipping cost, discount ordering), Symfony's service decoration is safer than subclassing:
App\Shop\DecoratingPricingCalculator:
decorates: softspring_shop.pricing_calculator
arguments: ['@.inner']
You keep the bundle's wiring intact and can remove the decorator in one diff.
There is essentially no documentation. The README is one line. Plan on reading source, and write down what you learn in a docs/shop-bundle-notes.md in your repo — the next developer will need it, and if you fork, it becomes your README. Assume no Stack Overflow coverage and no community answers.
Migrations will collide. The bundle likely ships migrations or relies on doctrine:schema:update. Before running anything, compare doctrine:migrations:diff output on a scratch DB. If the bundle's schema and yours disagree on table names, join tables, or column types, you have two options: let the bundle own the schema and build around it (greenfield, clean), or map the bundle's entities to different table names via your config and keep your tables separate (retrofit, ugly but survivable). Never retrofit into a live orders table without an explicit plan.
Check the version constraints before you fall in love. composer why-not symfony/framework-bundle 7.0 (or whatever your next LTS is) tells you immediately whether this bundle blocks your upgrade path. An abandoned package that pins symfony/* ^5.4 becomes a security-patch tax the year you want to move to 7.x. This is the most expensive long-term gotcha and it costs 30 seconds to check.
Config quirks: the alias is not always what you guess. softspring_shop vs softspring_shop_bundle vs shop. Use bin/console config:dump-reference | grep -i shop and debug:config to find the real one; a wrong alias silently means your config is ignored, and you will spend an afternoon wondering why your class: override has no effect.
Cart state may be session-bound. Look at how the cart is loaded (CartManager::getCurrentCart() or a CartListener on kernel.request). If it reads from the session, you cannot horizontally scale without sticky sessions, and multi-device cart continuity is not free. Check for a cart persistence layer before promising the feature to a product owner.
Payment is the highest-risk seam. Determine immediately whether the bundle defines a PaymentProviderInterface (good — you plug in Stripe/Adyen yourself and keep PCI scope out of the bundle) or hardcodes a gateway (bad — it drags you into PCI scope). Search for Payment in src/ and read the abstraction. Under no circumstances let payment credentials or card data flow through bundle code you have not audited; keep checkout completion in your own service and use the bundle only for the order record.
Checkout partial-failure states are on you. Unmaintained bundles rarely have idempotent order placement. Wrap order creation in your own transaction, use an idempotency key on the PSP call, and add a reconciliation job for orders stuck in pending. Test the "PSP times out after order row inserted" case explicitly — this is the classic silent-revenue-loss bug.
Zero stars means zero downstream validation of the install path. Do not trust the recipe to have run correctly. After install, git diff your config/bundles.php, config/routes.yaml, and config/packages/ — Flex recipes can silently overwrite routing or add a when@dev block you did not expect.
Fork on day one if you adopt. With no dependents, upstream is not a safety net. Fork under your org, pin the version in composer.json, and add a UPSTREAM.md noting the commit you forked from and the steps to rebase. That note is the difference between a manageable fork and a mystery.
Debug tactic when services are missing. bin/console debug:container --tag=<tag> for whatever tags appear in services.yaml, and debug:autowiring for the class names. If a manager is not autowirable, either it is not aliased or it is public: false with no interface — in which case decorate a public one or grab it from the container in a command to introspect it.
Ramp-up reality for a Laravel developer. If you are coming from Laravel, budget one to two weeks of Symfony onboarding — bundles, the DI container's compiler passes, Doctrine mappings, Twig namespacing — before this bundle's own quirks. That onboarding cost is real and should be weighed against just using Sylius, which is maintained and documented.
How can I help you explore Laravel packages today?