Which Laravel e-commerce CMS/package is the best? It's a common question on forums. We tested out FOUR popular Laravel packages for e-shops - Aimeos, Bagisto, Lunar, and Vanilo. In this tutorial, you will see who are the "winners".
When talking about e-commerce in general, the most popular choices are non-Laravel: systems like Magento, PrestaShop, WooCommerce, or you can just register at Shopify. But in Laravel, there are also quite a few alternatives you could use.
Each has its strengths and issues. We will compare the shops, based on these criteria:
Let's get started!
Before looking at a review, we should say this:
This review was done in July 2023, which means that the information about some of the systems may be outdated, so please check the latest information on the official websites.
With that out of the way, here's the overview of each system:
It's a whole Laravel application you can install, immediately giving you access to everything. It's also quite feature rich and has quite a few extensions available.
When looking at installation, you can instantly see two options - Composer installation with Laravel setup and GUI setup. We've tried the composer installation, and it worked without any issues. Once that is done, we get an entire store up and running:
Overall the experience of using this system has been great. It has a lot of features, and it's easy to use. Even product creation is quite simple as it's a wizard form:
As for the user side - there are a lot of features available. From wishlists to comparisons already built in. There are also a few options for payments ready to go:
Looking into this system from a developer's perspective had some unexpected twists! This is because you can access the code of the system as it's just inside a packages
directory:
Along with all the views you might want:
This allowed us to quickly figure out where things were and make any adjustments as needed. All the code and views were clear and understandable:
<?php namespace Webkul\Customer\Http\Controllers; use Illuminate\Database\Eloquent\ModelNotFoundException;use Illuminate\Support\Facades\Event;use Webkul\Customer\Http\Requests\CustomerAddressRequest;use Webkul\Customer\Repositories\CustomerAddressRepository; class AddressController extends Controller{ /** * Contains route related configuration. * * @var array */ protected $_config; /** * Current customer. * * @var \Webkul\Customer\Models\Customer */ protected $customer; /** * Create a new controller instance. * * @param \Webkul\Customer\Repositories\CustomerAddressRepository $customerAddressRepository * @return void */ public function __construct(protected CustomerAddressRepository $customerAddressRepository) { $this->_config = request('_config'); } /** * Address route index page. * * @return \Illuminate\View\View */ public function index() { $customer = auth()->guard('customer')->user(); return view($this->_config['view'])->with('addresses', $customer->addresses); } /** * Show the address create form. * * @return \Illuminate\View\View */ public function create() { return view($this->_config['view'], [ 'defaultCountry' => config('app.default_country'), ]); } /** * Create a new address for customer. * * @return view */ public function store(CustomerAddressRequest $request) { $customer = auth()->guard('customer')->user(); Event::dispatch('customer.addresses.create.before'); $customerAddress = $this->customerAddressRepository->create(array_merge($request->all(), [ 'customer_id' => $customer->id, 'address1' => implode(PHP_EOL, array_filter(request()->input('address1'))), 'default_address' => ! $customer->addresses->count(), ])); Event::dispatch('customer.addresses.create.after', $customerAddress); session()->flash('success', trans('shop::app.customer.account.address.create.success')); return redirect()->route($this->_config['redirect']); }// ...
@extends('shop::customers.account.index') @section('page_title') {{ __('shop::app.customer.account.address.index.page-title') }}@endsection @section('page-detail-wrapper') @if ($addresses->isEmpty()) <a href="{{ route('shop.customer.addresses.create') }}" class="theme-btn light unset address-button"> {{ __('shop::app.customer.account.address.index.add') }} </a> @endif <div class="account-head mt-3"> <span class="account-heading"> {{ __('shop::app.customer.account.address.index.title') }} </span> @if (! $addresses->isEmpty()) <span class="account-action"> <a href="{{ route('shop.customer.addresses.create') }}" class="theme-btn light unset float-right"> {{ __('shop::app.customer.account.address.index.add') }} </a> </span> @endif </div> {!! view_render_event('bagisto.shop.customers.account.address.list.before', ['addresses' => $addresses]) !!} <div class="account-table-content"> @if ($addresses->isEmpty()) <div>{{ __('shop::app.customer.account.address.index.empty') }}</div> @else <div class="row address-holder no-padding"> @foreach ($addresses as $address) <div class="col-lg-4 col-md-6 col-xs-12"> <div class="card m-1"> <div class="card-body"> <h5 class="card-title fw6">{{ $address->first_name }} {{ $address->last_name }}</h5> <ul type="none"> <li>{{ $address->address1 }}</li> <li>{{ $address->city }}</li> <li>{{ $address->state }}</li> <li>{{ core()->country_name($address->country) }} {{ $address->postcode }}</li> <li> {{ __('shop::app.customer.account.address.index.contact') }} : {{ $address->phone }} </li> </ul> <a class="card-link" href="{{ route('shop.customer.addresses.edit', $address->id) }}"> {{ __('shop::app.customer.account.address.index.edit') }} </a> <a class="card-link" href="javascript:void(0);" onclick="deleteAddress('{{ __('shop::app.customer.account.address.index.confirm-delete') }}', '{{ $address->id }}')"> {{ __('shop::app.customer.account.address.index.delete') }} </a> <form id="deleteAddressForm{{ $address->id }}" action="{{ route('shop.customer.addresses.delete', $address->id) }}" method="post"> @method('delete') @csrf </form> </div> </div> </div> @endforeach </div> @endif </div> {!! view_render_event('bagisto.shop.customers.account.address.list.after', ['addresses' => $addresses]) !!}@endsection @push('scripts') <script> function deleteAddress(message, addressId) { if (! confirm(message)) { return; } $(`#deleteAddressForm${addressId}`).submit(); } </script>@endpush @if ($addresses->isEmpty()) <style> a#add-address-button { position: absolute; margin-top: 92px; } .address-button { position: absolute; z-index: 1 !important; margin-top: 110px !important; } </style>@endif
It even has a full test suite available, so you can be sure you didn't break anything:
Overall, the system feels built to be adjusted in any way you need.
Bonus: It has PWA (progressive web application) and POS (point of sale) extensions available, which is quite nice.
We have played around with the system and have not noticed any bugs or significant issues. Any bigger problems can be manually optimized as the code is available. Or you can always ask the community for help.
Looking at the community, we've found quite a few articles available. But the best part - there's even a YouTube channel with tutorials available. There's also a GitHub repository that's quite active, a Forum with people.
Extension-wise, quite a few are from the official store.
Overall the Bagisto system was a pleasant surprise. It delivered a lot of functionality with a ton of customization available. And unlike others, it had a complete code available, which is a huge plus if you want to build something custom.
This e-commerce system is quite different as it is a "headless" system. This means that you should build your user interface on top of it. It does not have any UI out of the box, but they have a starter kit available that we can use!
Important note: LunarPHP is still pre-version 1.0, meaning things can change.
Since we want to launch a shop as soon as possible - we've chosen to install the starter kit. It's as simple as cloning a repository based on instructions. This worked quite well, and we didn't have any issues. It also comes with an interactive installer:
Once installed, we were able to run a database seeder to get demo products and categories:
Overall experience using the system was quite pleasant. It is fast and responsive (thanks to Livewire). All the systems worked as expected, and we even had a checkout that was working:
It even comes with Stripe integration out of the box, which is quite lovely, and you only need to add your API keys to get it working.
Looking at the Development experience, I was surprised as the starter kit had every Livewire component available in the directories you expect:
And same went with the design:
With code looking just like you would expect:
<form wire:submit.prevent="save" class="border rounded shadow-lg"> <div class="flex justify-between p-4 font-medium"> <span class="text-xl">Shipping Option</span> </div> @if ($this->shippingAddress) <div class="p-4 border-t"> @foreach ($this->shippingOptions as $option) <label class="flex items-center w-full cursor-pointer" wire:key="shipping_option_{{ $option->getIdentifier() }}"> <input type="radio" wire:model="chosenOption" value="{{ $option->getIdentifier() }}" /> <div class="flex items-center ml-2"> <span class="block mr-2 text-2xl">{{ $option->getPrice()->formatted() }}</span> {{ $option->getDescription() }} </div> </label> @endforeach </div> @else @endif @if ($errors->has('chosenOption')) <p class="p-4 text-sm text-red-500">{{ $errors->first('chosenOption') }}</p> @endif <div class="flex justify-end w-full p-4 bg-gray-100"> <div> <button type="submit" wire:key="submit_btn" class="px-5 py-3 font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-500"> Continue </button> </div> </div></form>
Along with these fantastic storefront components, you have decent documentation available to build anything. Just be warned that this system is **pretty new and has yet to reach version 1.0 **, so there are some things that need to be added!
This one is a bit tough to comment on. We could not find its extensions or themes yet (and it might not be there, given its headless nature). But there are GitHub repository where people can ask questions. Besides that, it's too early to comment on the community side.
We can only add a few comments here as we did not build the UI ourselves. The UI they have as a starter kit seems sufficient, but they state that it's not production-ready and might have issues:
That said, we did not encounter any bugs ourselves. The only problems we faced were misconfiguration of the system, which we fixed by adjusting settings.
Given its early days, my thoughts on the system are pretty good. It is fast, working, and has a lot of potential. But overall, as the developers stated on their website - it's in the rapid development stage, so expect breaking things.
Looking at their homepage, we've had a little impression that it might be outdated, but we were wrong. It's active and has all the things you might want there.
Installation was relatively easy when following their guide. Running a composer command started the setup, and it interactively asked for the database credentials:
Once that was done, it asked for the admin credentials:
And that was it. The installation was done, now loading the page already had demo products and everything:
Overall the shop experience was quite good. The shop is fast, responsive, and has almost all the features you might want. It does cover the majority of shop features and even comes with a few plugins for payment:
It even has a friendly admin panel with a lot of statistics:
So it is definitely a good starting point for a shop.
If we look at the development side - things could be looking better. We all know that shops should be highly customizable to adapt to your client's needs. Yet, this system provides little to no customization. Except for extensions, of course, those are available. But if you want to get your hands into the system and modify the code and adapt it to make your own - then it's a challenge. Here's why:
The system is built on top of Laravel but acts like a package. So you have the majority of your configuration inside of the config/shop.php
file:
$routes = [];$prefix = config( 'app.shop_multilocale' ) ? '{locale}/' : ''; if( config( 'app.shop_multishop' ) ) { $routes = ['routes' => [ 'admin' => ['prefix' => 'admin', 'middleware' => ['web']], 'jqadm' => ['prefix' => 'admin/{site}/jqadm', 'middleware' => ['web', 'auth', 'verified']], 'jsonadm' => ['prefix' => 'admin/{site}/jsonadm', 'middleware' => ['web', 'auth', 'verified']], 'jsonapi' => ['prefix' => '{site}/jsonapi', 'middleware' => ['web', 'api']], 'account' => ['prefix' => $prefix . '{site}/profile', 'middleware' => ['web', 'auth', 'verified']], 'default' => ['prefix' => $prefix . '{site}/shop', 'middleware' => ['web']], 'supplier' => ['prefix' => $prefix . '{site}/s', 'middleware' => ['web']], 'page' => ['prefix' => $prefix . '{site}/p', 'middleware' => ['web']], 'home' => ['prefix' => $prefix . '{site}', 'middleware' => ['web']], 'update' => ['prefix' => '{site}'], ] ];} return $routes + [ 'apc_enabled' => false, // enable for maximum performance if APCu is available 'apc_prefix' => 'aimeos:', // prefix for caching config and translation in APCu 'num_formatter' => 'Locale', // locale based number formatter (alternative: "Standard") 'pcntl_max' => 4, // maximum number of parallel command line processes when starting jobs 'version' => env( 'APP_VERSION', 1 ), // shop CSS/JS file version 'routes' => [ // Docs: https://aimeos.org/docs/latest/laravel/extend/#custom-routes // Multi-sites: https://aimeos.org/docs/latest/laravel/customize/#multiple-shops 'admin' => ['prefix' => 'admin', 'middleware' => ['web']], 'jqadm' => ['prefix' => 'admin/{site}/jqadm', 'middleware' => ['web', 'auth']], 'jsonadm' => ['prefix' => 'admin/{site}/jsonadm', 'middleware' => ['web', 'auth']], 'jsonapi' => ['prefix' => 'jsonapi', 'middleware' => ['web', 'api']], 'account' => ['prefix' => $prefix . 'profile', 'middleware' => ['web', 'auth']], 'default' => ['prefix' => $prefix . 'shop', 'middleware' => ['web']], 'supplier' => ['prefix' => $prefix . 's', 'middleware' => ['web']], 'page' => ['prefix' => $prefix . 'p', 'middleware' => ['web']], 'home' => ['prefix' => $prefix, 'middleware' => ['web']], 'update' => [], ], // ...
Yet, there are no Controllers, Models, or anything else you can access or extend only by code. You have to build extensions for it. The same goes for themes, but it gets a bit worse. The theme is a package, which is understandable but hard to extend. Let's look at the Basket to see what we get:
@extends('laraveldaily::base') @section('aimeos_header') <title>{{ __( 'Basket') }}</title> <?= $aiheader['locale/select'] ?? '' ?> <?= $aiheader['catalog/search'] ?? '' ?> <?= $aiheader['catalog/tree'] ?? '' ?> <?= $aiheader['basket/bulk'] ?? '' ?> <?= $aiheader['basket/standard'] ?? '' ?> <?= $aiheader['basket/related'] ?? '' ?>@stop @section('aimeos_head_nav') <?= $aibody['catalog/tree'] ?? '' ?>@stop @section('aimeos_head_locale') <?= $aibody['locale/select'] ?? '' ?>@stop @section('aimeos_head_search') <?= $aibody['catalog/search'] ?? '' ?>@stop @section('aimeos_body') <div class="container-fluid"> <?= $aibody['basket/standard'] ?? '' ?> <?= $aibody['basket/related'] ?? '' ?> <?= $aibody['basket/bulk'] ?? '' ?> </div>@stop
As you can see, a lot of data comes from inside the e-commerce shop, and what is where needs to be clarified. This is for all the pages:
So from a developer perspective, you are always locked into extensions. This is completely fine, but it might not be what you are looking for, as there are more popular systems out there!
Some extensions are listed on their extensions page, but looking around, it might not be enough. And quite a few of them are paid (we haven't tested them). So take a look and decide for yourself.
As for community, googling this system wasn't getting a lot of results back, even from StackOverflow:
And the primary source of community that's active - their own forum.
Looking at the reliability aspect, it worked pretty well. There were some hiccups along the way like products not loading once you selected a variant. But apart from that, everything in the demo worked well, and even the admin panel was relatively fast.
Overall the experience was decent. It might not fit your needs if you are in the market for a system that you can modify and extend upon, but if all you need is a quick store - it might be a good fit. Look at the extensions and themes to see if they fit your needs.
This one is different overall from what we've seen so far. My initial impression is that it's a Laravel package that provides you with the shop's functionality.
But there was an issue. We've encountered inconsistencies in their documentation.
This asked us to install a Laravel UI package that is no longer officially recommended with Laravel 10. While switching to Breeze and installing their admin interface, there was another problem - the tutorial requires mix
to be installed, but Laravel has switched to vite
:
This might not be a deal-breaker, but you must switch to mix
. In our case, this is not something we wanted to do, as we wanted to test the system "as is".
Overall, the system is supposed to be a headless version (as we didn't find any reference to it if we missed it - let us know!), which meant that we would need to create our own front end. And for the system that gives problems at the installation, we decided not to spend more time on it.
Vanilo will get an "honorable mention" on our list but won't get a full review. Seems like the system was a good idea a few years ago but never reached the "production level".
We should be fair that we have yet to launch these systems in production, but some of them instantly raised red flags for us from a usability or extensibility perspective.
Our favorite one was Bagisto, as it had a lot of functionality available, was easy to customize, and we could look at the code. Add a bonus of it being open-source, and you have a great shop open.
If you want to dive deeper into Bagisto, we also have published this step-by-step tutorial: Laravel E-Shop with Bagisto: Custom Design Theme
We can't say Bagisto is the best system out there, but at least the first experience is the most pleasant out of all four options tested here. Would you agree? What is your experience?