Developing a Custom Bagisto Package
Bagisto is built on the concept of Laravel packages: each functional unit is an isolated module with its own models, controllers, routes, and views. A custom package allows adding any logic without touching vendor code and without breaking core updates.
Package structure
packages/
└── MyCompany/
└── Catalog/
├── src/
│ ├── Http/
│ │ ├── Controllers/
│ │ │ └── Admin/CatalogController.php
│ │ └── Requests/
│ ├── Models/
│ │ └── CustomAttribute.php
│ ├── Repositories/
│ │ └── CustomAttributeRepository.php
│ ├── Database/
│ │ ├── Migrations/
│ │ └── Seeders/
│ ├── Resources/
│ │ ├── assets/
│ │ │ ├── js/
│ │ │ └── css/
│ │ └── views/
│ │ └── admin/
│ ├── Config/
│ │ └── menu.php
│ ├── Routes/
│ │ ├── admin.php
│ │ └── shop.php
│ └── Providers/
│ └── CatalogServiceProvider.php
└── composer.json
Package registration
Root project composer.json:
{
"autoload": {
"psr-4": {
"MyCompany\\Catalog\\": "packages/MyCompany/Catalog/src"
}
}
}
composer dump-autoload
ServiceProvider — package entry point:
<?php
namespace MyCompany\Catalog\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Event;
class CatalogServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->mergeConfigFrom(
__DIR__.'/../Config/menu.php', 'menu'
);
}
public function boot(): void
{
$this->loadRoutesFrom(__DIR__.'/../Routes/admin.php');
$this->loadRoutesFrom(__DIR__.'/../Routes/shop.php');
$this->loadMigrationsFrom(__DIR__.'/../Database/Migrations');
$this->loadViewsFrom(__DIR__.'/../Resources/views', 'mycompany-catalog');
$this->loadTranslationsFrom(__DIR__.'/../Resources/lang', 'mycompany-catalog');
$this->publishes([
__DIR__.'/../Resources/assets' => public_path('vendor/mycompany/catalog'),
], 'mycompany-catalog-assets');
// Subscribe to core events
Event::listen(
'catalog.product.create.after',
'MyCompany\Catalog\Listeners\ProductCreatedListener'
);
}
}
Register in config/app.php:
'providers' => [
// ...
MyCompany\Catalog\Providers\CatalogServiceProvider::class,
],
Repositories instead of direct queries
Bagisto uses Repository pattern over Eloquent. Custom repositories inherit from the core base class:
<?php
namespace MyCompany\Catalog\Repositories;
use Webkul\Core\Eloquent\Repository;
use MyCompany\Catalog\Models\CustomAttribute;
class CustomAttributeRepository extends Repository
{
public function model(): string
{
return CustomAttribute::class;
}
public function getByProduct(int $productId): \Illuminate\Support\Collection
{
return $this->where('product_id', $productId)
->where('is_active', true)
->orderBy('sort_order')
->get();
}
}
Register in ServiceProvider:
$this->app->bind(
\MyCompany\Catalog\Repositories\CustomAttributeRepository::class
);
Adding admin menu item
// Config/menu.php
return [
[
'key' => 'mycompany',
'name' => 'MyCompany',
'route' => 'mycompany.catalog.index',
'sort' => 10,
'icon' => 'icon-catalog',
],
[
'key' => 'mycompany.catalog',
'name' => 'Catalog',
'route' => 'mycompany.catalog.index',
'sort' => 1,
'icon' => '',
],
];
Vue components in package
Bagisto 2.x uses Vue 3 + Vite. Package components are registered globally:
// packages/MyCompany/Catalog/src/Resources/assets/js/index.js
import CustomAttributeForm from './components/CustomAttributeForm.vue';
export default {
install(app) {
app.component('custom-attribute-form', CustomAttributeForm);
}
}
Connection in root project's vite.config.js via resolve.alias or through asset publishing.
Intercepting core events
Bagisto generates events at key points — this is the main extension mechanism without patching:
| Event | When triggered |
|---|---|
checkout.order.save.after |
After order creation |
catalog.product.create.after |
After product creation |
customer.registration.after |
After customer registration |
sales.invoice.save.after |
After invoice creation |
catalog.product.update.after |
After product update |
<?php
namespace MyCompany\Catalog\Listeners;
class ProductCreatedListener
{
public function handle($product): void
{
// Queue for Elasticsearch indexing
dispatch(new \MyCompany\Catalog\Jobs\IndexProduct($product->id));
}
}
Testing the package
<?php
namespace MyCompany\Catalog\Tests\Unit;
use Tests\TestCase;
use MyCompany\Catalog\Repositories\CustomAttributeRepository;
class CustomAttributeRepositoryTest extends TestCase
{
public function test_returns_active_attributes(): void
{
$repo = app(CustomAttributeRepository::class);
$attrs = $repo->getByProduct(1);
$this->assertInstanceOf(\Illuminate\Support\Collection::class, $attrs);
$this->assertTrue($attrs->every(fn($a) => $a->is_active));
}
}
Development timeline
| Package type | Timeline |
|---|---|
| Simple (CRUD + menu) | 1-3 days |
| Third-party API integration | 3-7 days |
| Custom checkout flow | 1-2 weeks |
| Full module (marketplace vendor) | 2-4 weeks |
The package is formatted with composer.json, tests, and documentation — this is important when handing to the team or updating Bagisto to the next major version.







