Unopim plugin development
Agent skills for UnoPim — domain-specific context for AI agents (Claude Code, Cursor, Windsurf) working in an UnoPim PIM codebase. Install: npx skills add unopim/agent-skills
npx -y skills add unopim/agent-skills --skill unopim-plugin-developmentAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Plugin and package development for UnoPim. Activates when creating modules/packages, service providers, models, repositories, controllers, routes, ACL, menus, migrations, credentials, cURL HTTP clients, attribute mapping, or scaffolding a third-party connector (WooCommerce, Shopify, Shopware, REST API). Use references: @quickstart (day-1 connector checklist), @core (package structure, service providers, config), @backend (PHP classes, models, repositories, events), @credentials (credential storage, connection testing, history), @http (cURL ApiClient, BasicAuth/OAuth), @mapping (attribute mapping + history).
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
60.3 KB, ~14.1k tokens by cl100k_base, as published. Nobody here has run it
Plugin Development in UnoPim
Overview
A plugin (module/package) is a self-contained Concord module that adds features to UnoPim. This comprehensive skill covers the full lifecycle of building an UnoPim module or third-party connector — from day-1 scaffold to credentials, HTTP client, and attribute mapping.
When to Apply
Activate this skill when:
- Scaffolding a new UnoPim module or connector (WooCommerce, Shopify, Shopware, any REST API)
- Creating service providers, routes, controllers, repositories, models
- Writing migrations (
Database/Migration/singular,wk_/DB_PREFIXtables) - Configuring ACL (flat arrays), admin menus, and system config
- Building credential storage with connection testing and history
- Writing a cURL-based
ApiClient(NOT Guzzle / Laravel HTTP) - Implementing attribute export mapping and mapping history
Critical Conventions (apply throughout)
- Table prefix via
DB_PREFIXin.env(defaultwk_); use unprefixed names inDB::table(). - Migrations in
Database/Migration/(singular). - Route middleware
['admin']only — never['web', 'admin']. - Models use
HistoryTrait+ implementPresentableHistoryInterface. - Controllers return
JsonResponsewithredirect_url+message. - Sensitive fields go in
$auditExclude— neverCrypt::encryptString(). - Connectors use native cURL — not Guzzle, not the Laravel Http facade.
- Comments: write Laravel-core style. Code self-documents; no inline comments narrating what a line does, no multi-line essays inside methods, no section-divider comments. Comment only a non-obvious why, one line — or use the method PHPDoc.
- ACL config = flat arrays (no nested
children). - No hardcoded user-facing strings — use
trans('package::file.key').
@quickstart: Day-1 Connector Quickstart
Reference Implementation
All patterns in this guide are verified against the production WooCommerce
connector: packages/Webkul/WooCommerce/. When in doubt, refer to that
package's source code.
Prerequisites (15 min)
Admin UI Rule (CRITICAL)
For all admin Blade forms in connector modules, use UnoPim Blade components first.
- Use
x-admin::form.control-groupwrappers. - Use
x-admin::form.control-group.labelandx-admin::form.control-group.error. - Use
x-admin::form.control-group.controlfor inputs/selects/textareas. - Do not generate raw
<select>,<input>,<textarea>, or<label>markup when component equivalents are available. - Use translations for all labels/placeholders/messages.
For select fields, use component select (type="select") with :options="json_encode(...)", track-by, label-by, and Vue @input binding.
Before coding:
- Confirm the external API authentication type (Basic Auth / Bearer Token / OAuth)
- Note the API base URL pattern (e.g.
/wp-json/wc/v3/,/api/v1/, etc.) - Decide what entities to sync: Products, Categories, Attributes, Customers, Orders
- List which direction: export only / import only / bidirectional
Phase 1 — Scaffolding (1–2 hours)
Step 1: Create directory structure
packages/Webkul/{ModuleName}/
├── Config/
├── Routes/
├── Database/
│ ├── Migration/ ← NOT "Migrations" (singular)
│ └── Factories/
├── Resources/
│ ├── lang/en/
│ └── views/
└── src/
├── Providers/
├── Models/
├── Contracts/
├── Repositories/
├── Http/
│ ├── Controllers/
│ ├── Requests/
│ └── Client/
├── DataGrids/
│ └── Credential/
├── Services/
├── Helpers/
│ ├── Exporters/
│ │ ├── Product/
│ │ ├── Category/
│ │ └── Attribute/
│ └── Importers/
│ └── Product/
├── Validators/
│ └── JobInstances/
│ ├── Export/
│ └── Import/
└── Presenters/
Step 2: composer.json
{
"name": "webkul/{module-name}",
"type": "library",
"autoload": {
"psr-4": { "Webkul\\{ModuleName}\\": "src/" }
},
"extra": {
"laravel": {
"providers": ["Webkul\\{ModuleName}\\Providers\\{ModuleName}ServiceProvider"]
}
}
}
Step 3: {ModuleName}ServiceProvider
Critical rules:
- Routes:
Route::middleware('web')->group(...)— NOT$this->loadRoutesFrom() - Event:
unopim.admin.layout.head.before— NOT without.before - Register
ModuleServiceProviderinsideregister()
public function boot(): void
{
Route::middleware('web')->group(__DIR__ . '/../../Routes/{module-name}-routes.php');
$this->loadViewsFrom(...);
$this->loadTranslationsFrom(...);
$this->loadMigrationsFrom(__DIR__ . '/../../Database/Migration');
Event::listen('unopim.admin.layout.head.before', function ($viewRenderEventManager) {
$viewRenderEventManager->addTemplate('{module-name}::layouts.head');
});
}
public function register(): void
{
$this->app->register(ModuleServiceProvider::class);
$this->registerConfig();
}
protected function registerConfig(): void
{
$this->mergeConfigFrom(..., 'acl');
$this->mergeConfigFrom(..., 'menu');
$this->mergeConfigFrom(..., 'exporters');
$this->mergeConfigFrom(..., 'quick_exporters');
$this->mergeConfigFrom(..., 'importers');
}
Step 4: ModuleServiceProvider
class ModuleServiceProvider extends CoreModuleServiceProvider
{
protected $models = [
\Webkul\{ModuleName}\Models\Credential::class,
];
}
Phase 2 — Database & Model (1 hour)
Step 5: Migration
- File in:
Database/Migration/(NOTDatabase/Migrations/) - Table name:
{module}_credentials(DB_PREFIX added automatically by Laravel) - Column names: camelCase matching model
$fillable(e.g.apiUrl,consumerKey) - Include
extrasJSON column for flexible extra config
Schema::create('{module}_credentials', function (Blueprint $table) {
$table->id();
$table->string('label');
$table->string('apiUrl');
$table->string('consumerKey');
$table->string('consumerSecret');
$table->json('extras')->nullable();
$table->boolean('status')->default(true);
$table->timestamps();
});
Step 6: Contract Interface
src/Contracts/Credential.php ← interface Credential {}
Step 7: Credential Model
class Credential extends Model implements CredentialContract, PresentableHistoryInterface
{
use HasFactory, HistoryTrait;
protected $table = '{module}_credentials';
protected $casts = ['extras' => 'array', 'status' => 'boolean'];
protected $auditExclude = ['consumerSecret']; // never Crypt::encryptString
}
Step 8: CredentialRepository
class CredentialRepository extends Repository
{
public function model(): string { return Credential::class; }
}
Phase 3 — HTTP Client (30 min)
Step 9: cURL-based ApiClient
Unopim connectors use native cURL — NOT Guzzle, NOT Laravel HTTP facade.
src/Http/Client/
├── ApiClient.php ← curl_init / curl_exec / curl_close
└── BasicAuth.php ← curl_setopt(CURLOPT_HTTPAUTH)
Key methods: configure(), buildApiUrl(), get(), post(), put(), delete()
Step 10: Service class wraps all API calls
src/Services/{ModuleName}Service.php
Controllers inject {ModuleName}Service, never ApiClient directly.
Phase 4 — CRUD Controller & Routes (1 hour)
Step 11: FormRequest
src/Http/Requests/CredentialForm.php
Never use inline $request->validate() in controllers.
Step 12: CredentialController
- Returns
JsonResponsewithredirect_urlfor store/update/delete - Inject
CredentialRepositoryand{ModuleName}Service testConnection()method for API connectivity check
Step 13: Routes
// Middleware: ['admin'] only — NOT ['web', 'admin']
Route::group(['middleware' => ['admin'], 'prefix' => config('app.admin_url')], function () {
// credentials CRUD + test-connection
// credentials/get ← required for exporters filter list_route
});
Phase 5 — Admin UI (1 hour)
Step 14: ACL (flat array — no nested children)
return [
['key' => '{module-slug}', 'name' => '...', 'route' => '...', 'sort' => 1],
['key' => '{module-slug}.credentials', ...],
['key' => '{module-slug}.credentials.create', ...],
['key' => '{module-slug}.credentials.edit', ...],
['key' => '{module-slug}.credentials.delete', ...],
];
Step 15: DataGrid
src/DataGrids/Credential/CredentialDataGrid.php
Rules:
DB::table('{module}_credentials')inprepareQueryBuilder()- Methods have PHPDoc
@returnonly (no PHP type hints) - Column
closureusesfn ($row) =>(arrow function) - Action
urlusesfunction ($row) { return route(...); }(regular function) - Status badges:
label-active/label-info text-gray-600 dark:text-gray-300
Step 16: Blade views
views/credentials/index.blade.php ← <x-admin::datagrid :src="route(...)" />
views/credentials/create.blade.php
views/credentials/edit.blade.php
Phase 6 — Export/Import Workflow (2 hours)
Step 17: exporters.php (every select field MUST have these 4 keys)
[
'name' => 'credential',
'type' => 'select',
'async' => true, ← required
'track_by' => 'id', ← required
'label_by' => 'label', ← required
'list_route' => '{module-slug}.credentials.get', ← required
]
Step 18: quick_exporters.php (one-click from product listing)
'{ModuleName}QuickExport' => [
'title' => '...',
'route' => '{module-slug}.quick_export',
'exporter' => ...,
'source' => \Webkul\Product\Repositories\ProductRepository::class,
];
Step 19: importers.php (same filter field structure as exporters)
Step 20: Exporter & Importer classes
src/Helpers/Exporters/{Entity}/Exporter.php ← extends AbstractExporter
src/Helpers/Importers/{Entity}/Importer.php ← extends AbstractImporter
Each exporter needs: BATCH_SIZE, UNOPIM_ENTITY_NAME, ACTION_ADD, ACTION_UPDATE, CODE_ALREADY_EXIST, CODE_NOT_EXIST
Step 21: Validator classes
src/Validators/JobInstances/Export/ProductsValidator.php
src/Validators/JobInstances/Import/ProductsValidator.php
Phase 7 — Lang & Final Polish (30 min)
Step 22: Translation file
// Resources/lang/en/app.php
return [
'credentials' => [
'create-success' => 'Credential created successfully.',
'update-success' => 'Credential updated successfully.',
'delete-success' => 'Credential deleted successfully.',
'test-success' => 'Connection successful.',
'test-failed' => 'Connection failed. Check credentials.',
],
// acl, menu, data-transfer keys...
];
Step 23: Register provider
Add to bootstrap/providers.php:
\Webkul\{ModuleName}\Providers\{ModuleName}ServiceProvider::class,
Complete Checklist (in order)
Scaffolding
-
composer.jsonwith correct PSR-4 autoload -
{ModuleName}ServiceProviderwithRoute::middleware('web')->group() - Event:
unopim.admin.layout.head.before(with.before) -
ModuleServiceProviderextendsCoreModuleServiceProviderwith$models[] - All 5 configs merged in
registerConfig()
Database
- Migration in
Database/Migration/(NOTMigrations) - Tables use DB_PREFIX (auto-added by Laravel)
- camelCase column names match model
$fillable -
extrasJSON column present
Model
-
HistoryTrait+PresentableHistoryInterface -
$auditExcludefor sensitive fields -
'extras' => 'array'cast - Matching
Contracts/interface
Routes & Controller
- Route middleware:
['admin']only -
credentials/getroute for exporterlist_route - Controller uses
FormRequest(no inline validate) - Controller returns
JsonResponsewithredirect_url
HTTP Client
-
ApiClient.phpuses cURL (no Guzzle) -
BasicAuth.phpor equivalent auth class -
{ModuleName}Service.phpwraps all API calls
DataGrid
- Subdirectory:
DataGrids/Credential/ - PHPDoc
@returnonly (no PHP type hints on methods) - Column
closureusesfn ($row) => - Action
urlusesfunction ($row) { return ...; }
Export/Import
-
exporters.phpfilter fields haveasync/track_by/label_by/list_route -
quick_exporters.phpcreated -
importers.phpcreated - Exporter classes in
Helpers/Exporters/{Entity}/ - Validator classes in
Validators/JobInstances/
ACL
- Flat array (no nested
children) - Permission checks use
bouncer()->hasPermission()
Skill Cross-Reference
| Topic | Skill to use |
|---|---|
| Full module scaffold | unopim-package |
| Credential CRUD + model | unopim-credential-management |
| cURL HTTP client | unopim-http-client |
| Export/import jobs | unopim-export-workflow |
| DataGrid listing | unopim-datagrid |
| module mapping | unopim-connector-export-mapping |
@core: Package Structure & Scaffold
Overview
All Unopim third-party connector packages follow the same structure inspired by
packages/Webkul/WooCommerce/. Every pattern here is derived from that
production reference implementation.
Vendor/Module convention: Webkul/{ModuleName} where {ModuleName} is
PascalCase (e.g. WooCommerce, Shopify, module, ShopwareIntegration).
Admin UI rule (critical):
- For admin Blade forms, always use UnoPim Blade components (
x-admin::form.control-group,.label,.control,.error). - Do not generate raw
<select>,<input>,<textarea>, or<label>controls when a component equivalent exists. - Keep all user-facing text in translation keys.
- For select fields, use component select with
type="select",:options="json_encode(...)",track-by,label-by, and Vue@input.
1. Directory Structure
packages/Webkul/{ModuleName}/
├── composer.json
├── Config/
│ ├── acl.php
│ ├── menu.php
│ ├── exporters.php
│ ├── quick_exporters.php
│ └── importers.php
├── Routes/
│ └── {module-name}-routes.php
├── Database/
│ ├── Migration/ # NOT "Migrations" — singular
│ │ └── 2025_01_01_000000_{module}_credentials.php
│ └── Factories/
│ └── CredentialFactory.php
├── Resources/
│ ├── lang/
│ │ └── en/
│ │ └── app.php
│ └── views/
│ ├── credentials/
│ │ ├── index.blade.php
│ │ ├── create.blade.php
│ │ └── edit.blade.php
│ └── export/
│ └── export.blade.php
└── src/
├── Providers/
│ ├── {ModuleName}ServiceProvider.php # Main provider
│ └── ModuleServiceProvider.php # CoreModuleServiceProvider
├── Models/
│ ├── Credential.php
│ └── ...
├── Contracts/
│ ├── Credential.php # Interface per model
│ └── ...
├── Repositories/
│ ├── CredentialRepository.php
│ └── ...
├── Http/
│ ├── Controllers/
│ │ ├── CredentialController.php
│ │ └── ...
│ ├── Requests/
│ │ ├── CredentialForm.php # FormRequest class
│ │ └── ...
│ └── Client/
│ ├── ApiClient.php # cURL-based HTTP client
│ └── BasicAuth.php
├── DataGrids/
│ └── Credential/
│ └── CredentialDataGrid.php # Always in subdirectory
├── Services/
│ └── {ModuleName}Service.php # Wraps all API calls
└── Presenters/
└── CredentialPresenter.php # For HistoryControl display
2. composer.json
{
"name": "webkul/{module-name}",
"description": "{Module Name} integration for Unopim",
"type": "library",
"require": {
"php": "^8.1"
},
"autoload": {
"psr-4": {
"Webkul\\{ModuleName}\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"Webkul\\{ModuleName}\\Providers\\{ModuleName}ServiceProvider"
]
}
}
}
3. Main ServiceProvider
The main ServiceProvider handles booting routes, views, translations, events, and registering configs.
CRITICAL patterns:
- Routes must use
Route::middleware('web')->group(...)— NOT$this->loadRoutesFrom() - Event name for head injection is
unopim.admin.layout.head.before(with.before) - Six configs must be registered:
acl,menu,exporters,quick_exporters,importers, plus any custom ones loadTranslationsFromandloadViewsFromuse the module slug as the namespace
<?php
namespace Webkul\{ModuleName}\Providers;
use Illuminate\Routing\Router;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
class {ModuleName}ServiceProvider extends ServiceProvider
{
/**
* Bootstrap services.
*/
public function boot(Router $router): void
{
// Routes — must use Route::middleware('web')->group(), NOT loadRoutesFrom()
Route::middleware('web')->group(
__DIR__ . '/../../Routes/{module-name}-routes.php'
);
// Views
$this->loadViewsFrom(__DIR__ . '/../../Resources/views', '{module-name}');
// Translations
$this->loadTranslationsFrom(
__DIR__ . '/../../Resources/lang',
'{module-name}'
);
// Migrations
$this->loadMigrationsFrom(__DIR__ . '/../../Database/Migration');
// Inject assets/scripts into admin head
// IMPORTANT: event name ends with ".before"
Event::listen('unopim.admin.layout.head.before', function ($viewRenderEventManager) {
$viewRenderEventManager->addTemplate(
'{module-name}::layouts.head'
);
});
}
/**
* Register services.
*/
public function register(): void
{
$this->registerConfig();
}
/**
* Register config files.
*/
protected function registerConfig(): void
{
$this->mergeConfigFrom(__DIR__ . '/../../Config/acl.php', 'acl');
$this->mergeConfigFrom(__DIR__ . '/../../Config/menu.php', 'menu');
$this->mergeConfigFrom(__DIR__ . '/../../Config/exporters.php', 'exporters');
$this->mergeConfigFrom(__DIR__ . '/../../Config/quick_exporters.php', 'quick_exporters');
$this->mergeConfigFrom(__DIR__ . '/../../Config/importers.php', 'importers');
}
}
4. ModuleServiceProvider (Model Binding)
Extends Webkul\Core\Providers\CoreModuleServiceProvider. Only declare the
$models array — never write manual $app->bind() calls.
<?php
namespace Webkul\{ModuleName}\Providers;
use Webkul\Core\Providers\CoreModuleServiceProvider;
class ModuleServiceProvider extends CoreModuleServiceProvider
{
/**
* Models to register their repository bindings.
*
* @var array
*/
protected $models = [
\Webkul\{ModuleName}\Models\Credential::class,
// add more models here
];
}
Register both providers in {ModuleName}ServiceProvider::register():
public function register(): void
{
$this->app->register(ModuleServiceProvider::class);
$this->registerConfig();
}
5. Model (with HistoryTrait)
Every model must use HistoryTrait from Webkul\HistoryControl and implement
PresentableHistoryInterface. Sensitive fields must be excluded via
$auditExclude. Use an extras JSON column for flexible additional config.
<?php
namespace Webkul\{ModuleName}\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Webkul\{ModuleName}\Contracts\Credential as CredentialContract;
use Webkul\HistoryControl\Interfaces\PresentableHistoryInterface;
use Webkul\HistoryControl\Traits\HistoryTrait;
class Credential extends Model implements CredentialContract, PresentableHistoryInterface
{
use HasFactory, HistoryTrait;
/**
* Table name — always use DB_PREFIX.
*
* @var string
*/
protected $table = '{module}_credentials';
/**
* Fillable attributes.
*
* @var array
*/
protected $fillable = [
'label',
'apiUrl',
'consumerKey',
'consumerSecret', // secret — excluded from history below
'extras',
'status',
];
/**
* Casts.
*
* @var array
*/
protected $casts = [
'extras' => 'array',
];
/**
* Fields excluded from history audit.
* Sensitive values should never appear in history log.
*
* @var array
*/
protected $auditExclude = [
'consumerSecret',
];
/**
* History auditable attributes (subset shown in history UI).
*
* @var array
*/
protected $historyAuditable = [
'label',
'apiUrl',
'consumerKey',
'status',
];
}
6. Contract Interface
Every model must have a matching interface in src/Contracts/.
<?php
namespace Webkul\{ModuleName}\Contracts;
interface Credential
{
// marker interface — methods defined in model
}
7. Routes
Route middleware is ['admin'] only — NOT ['web', 'admin'].
Webhook routes (if any) must exclude VerifyCsrfToken.
<?php
use Illuminate\Support\Facades\Route;
use Webkul\{ModuleName}\Http\Controllers\CredentialController;
Route::group(['middleware' => ['admin'], 'prefix' => config('app.admin_url')], function () {
Route::prefix('{module-slug}')->name('{module-slug}.')->group(function () {
// Credentials
Route::get('credentials', [CredentialController::class, 'index'])->name('credentials.index');
Route::get('credentials/create', [CredentialController::class, 'create'])->name('credentials.create');
Route::post('credentials', [CredentialController::class, 'store'])->name('credentials.store');
Route::get('credentials/{id}/edit',[CredentialController::class, 'edit'])->name('credentials.edit');
Route::put('credentials/{id}', [CredentialController::class, 'update'])->name('credentials.update');
Route::delete('credentials/{id}', [CredentialController::class, 'destroy'])->name('credentials.destroy');
// Test connection endpoint
Route::post('credentials/test-connection', [CredentialController::class, 'testConnection'])
->name('credentials.test-connection');
});
});
// Webhook (no CSRF check)
Route::post('{module-slug}/webhook', [\Webkul\{ModuleName}\Http\Controllers\WebhookController::class, 'handle'])
->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class])
->name('{module-slug}.webhook');
8. ACL Config
Flat array structure — no nested children arrays.
<?php
return [
[
'key' => '{module-slug}',
'name' => '{module-name}::app.acl.{module-slug}',
'route' => '{module-slug}.credentials.index',
'sort' => 1,
],
[
'key' => '{module-slug}.credentials',
'name' => '{module-name}::app.acl.credentials',
'route' => '{module-slug}.credentials.index',
'sort' => 1,
],
[
'key' => '{module-slug}.credentials.create',
'name' => '{module-name}::app.acl.create',
'route' => '{module-slug}.credentials.create',
'sort' => 1,
],
[
'key' => '{module-slug}.credentials.edit',
'name' => '{module-name}::app.acl.edit',
'route' => '{module-slug}.credentials.edit',
'sort' => 2,
],
[
'key' => '{module-slug}.credentials.delete',
'name' => '{module-name}::app.acl.delete',
'route' => '{module-slug}.credentials.destroy',
'sort' => 3,
],
];
9. Menu Config
<?php
return [
[
'key' => '{module-slug}',
'name' => '{module-name}::app.menu.{module-slug}',
'route' => '{module-slug}.credentials.index',
'sort' => 5,
'icon' => 'icon-{module-slug}',
],
[
'key' => '{module-slug}.credentials',
'name' => '{module-name}::app.menu.credentials',
'route' => '{module-slug}.credentials.index',
'sort' => 1,
'icon' => '',
],
];
10. Migration
Table prefix is set via DB_PREFIX in .env (default: wk_). Folder is Database/Migration/ (NOT Migrations).
Column names use camelCase to match model $fillable exactly.
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('{module}_credentials', function (Blueprint $table) {
$table->id();
$table->string('label');
$table->string('apiUrl');
$table->string('consumerKey');
$table->string('consumerSecret');
$table->json('extras')->nullable(); // flexible JSON for extra config
$table->boolean('status')->default(true);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('{module}_credentials');
}
};
11. App Service Provider Registration
Register the package providers in the Unopim core config/app.php or via
package discovery. For local dev, add to bootstrap/providers.php:
\Webkul\{ModuleName}\Providers\{ModuleName}ServiceProvider::class,
12. Checklist for New Module
-
composer.jsoncreated with correct PSR-4 namespace -
{ModuleName}ServiceProviderusesRoute::middleware('web')->group(...) -
{ModuleName}ServiceProviderregistersModuleServiceProvider -
ModuleServiceProviderextendsCoreModuleServiceProviderwith$models[] - Event name:
unopim.admin.layout.head.before(never without.before) - Migration in
Database/Migration/(notMigrations) - Table names use DB_PREFIX (auto-added by Laravel)
- All models use
HistoryTrait+ implementPresentableHistoryInterface - All models have
$auditExcludefor sensitive fields - All models have
extrasJSON column with'extras' => 'array'cast - All models have matching
Contracts/interface - ACL is flat array (no nested
children) -
registerConfig()merges all 5 config files -
quick_exporters.phpandimporters.phpboth created - Route middleware is
['admin']only
Complete guide for creating, configuring, and deploying UnoPim plugins (packages).
When to Use This Skill
Invoke this skill when:
- Creating a new plugin/package for UnoPim
- Adding custom importers or exporters
- Extending admin menus, ACL, or system configuration
- Building custom DataGrids
- Creating custom models with Concord proxy pattern
Instructions
- Plugin structure: See plugin-structure.md for directory layout and boilerplate
- Service providers: See service-providers.md for registration and bootstrapping
- Config integration: See config-integration.md for menu, ACL, and system config
- Custom importers/exporters: See custom-data-transfer.md for import/export profiles
Quick Start
# 1. Create directory structure
mkdir -p packages/Webkul/Example/src/{Config,Contracts,Database/Migration,Http/Controllers,Models,Providers,Repositories,Resources/views,Routes}
# 2. Add PSR-4 autoload to composer.json
# "Webkul\\Example\\": "packages/Webkul/Example/src"
# 3. Register in config/app.php providers array
# Webkul\Example\Providers\ExampleServiceProvider::class,
# 4. Dump autoload
composer dump-autoload
# 5. Run migrations
php artisan migrate
Key Principles
- Always use the Concord proxy pattern for models
- Extend
Webkul\Core\Eloquent\Repositoryfor data access - Register configs via
mergeConfigFrom()inregister() - Load routes via
Route::middleware('web')->group(...)and migrations vialoadMigrationsFrom(.../Database/Migration)inboot() - Follow existing package structure conventions
- Add ACL entries for all new admin routes
@backend: Backend PHP Development
This skill provides guidance for developing UnoPim backend PHP code according to project standards and conventions.
When to Use This Skill
ALWAYS invoke this skill before:
- Writing new PHP tests (
*Test.phpfiles) - Creating new PHP classes, models, or repositories
- Modifying existing backend PHP code
- Adding events, listeners, or observers
- Working with Eloquent models and Concord proxies
Instructions
Follow UnoPim project conventions when adding or modifying backend PHP code:
- Coding style: See coding-conventions.md for Laravel/PSR standards
- Architecture patterns: See architecture.md for Concord, Repository, Strategy patterns
- Models and repositories: See models-repositories.md for Eloquent and Concord proxy patterns
- Events and listeners: See events-listeners.md for event-driven patterns
- Writing tests: See testing.md for Pest testing conventions
Key Principles
- Follow Laravel coding standards with
pint(Laravel preset) - Use the Repository pattern — all repos extend
Webkul\Core\Eloquent\Repository - Use Concord proxy models for model extensibility
- Place new code under the appropriate
packages/Webkul/{Package}/src/directory - Use PSR-4 autoloading:
Webkul\{Package}\namespace - Events follow
{domain}.{entity}.{action}.{before|after}naming - Product/category values use structured JSON with scoping (
common,locale_specific,channel_specific,channel_locale_specific) - Run
./vendor/bin/pintbefore committing - Run
./vendor/bin/pestto verify tests pass
@credentials: Credential Management
Overview
Credentials store API connection details for a third-party integration. All patterns are derived from the WooCommerce connector reference implementation.
Key rules:
- Table prefix: DB_PREFIX from .env (default wk_) — never hardcode in code
- Migration folder:
Database/Migration/(NOTMigrations) - Every model uses
HistoryTrait+ implementsPresentableHistoryInterface - Sensitive fields (API secrets, passwords) go in
$auditExclude— NOTCrypt::encryptString() - Flexible extra config uses
extrasJSON column (single column, not many columns) - Controllers return
JsonResponsewithredirect_urlfor store/update/delete - Use dedicated
Http/Requests/CredentialForm.phpFormRequest — not inline$request->validate() - A
Services/{ModuleName}Service.phpwraps all API calls
Admin UI rule (critical):
- For admin forms, use UnoPim components:
x-admin::form.control-group,.label,.control,.error. - Do not generate raw
<select>,<input>,<textarea>, or<label>when component equivalents exist. - Use translations for all user-facing form text.
- For dropdowns, use component select with
type="select",:options="json_encode(...)",track-by,label-by, and Vue@inputhandling.
1. Migration
<?php
// Database/Migration/2025_01_01_000000_{module}_credentials.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('{module}_credentials', function (Blueprint $table) {
$table->id();
$table->string('label');
$table->string('apiUrl');
$table->string('consumerKey');
$table->string('consumerSecret'); // stored plaintext; excluded from history
$table->string('storeId')->nullable();
$table->json('extras')->nullable(); // flexible JSON for additional config
$table->boolean('status')->default(true);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('{module}_credentials');
}
};
2. Contract Interface
<?php
// src/Contracts/Credential.php
namespace Webkul\{ModuleName}\Contracts;
interface Credential
{
// marker interface
}
3. Credential Model
<?php
// src/Models/Credential.php
namespace Webkul\{ModuleName}\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Webkul\{ModuleName}\Contracts\Credential as CredentialContract;
use Webkul\HistoryControl\Interfaces\PresentableHistoryInterface;
use Webkul\HistoryControl\Traits\HistoryTrait;
class Credential extends Model implements CredentialContract, PresentableHistoryInterface
{
use HasFactory, HistoryTrait;
/**
* Table — always DB_PREFIX.
*
* @var string
*/
protected $table = '{module}_credentials';
/**
* @var array
*/
protected $fillable = [
'label',
'apiUrl',
'consumerKey',
'consumerSecret',
'storeId',
'extras',
'status',
];
/**
* @var array
*/
protected $casts = [
'extras' => 'array',
'status' => 'boolean',
];
/**
* Fields excluded from audit history.
* Use this instead of Crypt::encryptString() for secrets.
*
* @var array
*/
protected $auditExclude = [
'consumerSecret',
];
/**
* Fields shown in history UI.
*
* @var array
*/
protected $historyAuditable = [
'label',
'apiUrl',
'consumerKey',
'storeId',
'status',
];
}
4. CredentialRepository
<?php
// src/Repositories/CredentialRepository.php
namespace Webkul\{ModuleName}\Repositories;
use Webkul\Core\Eloquent\Repository;
use Webkul\{ModuleName}\Contracts\Credential;
class CredentialRepository extends Repository
{
/**
* Specify model class.
*/
public function model(): string
{
return Credential::class;
}
}
5. FormRequest
Never use inline $request->validate() in the controller.
Use a dedicated Http/Requests/CredentialForm.php class.
<?php
// src/Http/Requests/CredentialForm.php
namespace Webkul\{ModuleName}\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class CredentialForm extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Validation rules.
*
* @return array<string, mixed>
*/
public function rules(): array
{
$credentialId = $this->route('id');
return [
'label' => 'required|string|max:255|unique:{module}_credentials,label' . ($credentialId ? ",{$credentialId}" : ''),
'apiUrl' => 'required|url',
'consumerKey' => 'required|string',
'consumerSecret' => $credentialId ? 'nullable|string' : 'required|string',
'status' => 'required|boolean',
];
}
/**
* Custom attribute names in messages.
*
* @return array<string, string>
*/
public function attributes(): array
{
return [
'apiUrl' => trans('{module-name}::app.credentials.api-url'),
'consumerKey' => trans('{module-name}::app.credentials.consumer-key'),
'consumerSecret' => trans('{module-name}::app.credentials.consumer-secret'),
];
}
}
6. CredentialController
Controllers must:
- Return
JsonResponsewithredirect_urlfor store/update/delete (not redirect()) - Guard with
bouncer()->hasPermission()for ACL - Use the FormRequest type-hint for automatic validation
<?php
// src/Http/Controllers/CredentialController.php
namespace Webkul\{ModuleName}\Http\Controllers;
use Illuminate\Http\JsonResponse;
use Illuminate\Routing\Controller;
use Webkul\{ModuleName}\DataGrids\Credential\CredentialDataGrid;
use Webkul\{ModuleName}\Http\Requests\CredentialForm;
use Webkul\{ModuleName}\Repositories\CredentialRepository;
use Webkul\{ModuleName}\Services\{ModuleName}Service;
class CredentialController extends Controller
{
public function __construct(
protected CredentialRepository $credentialRepository,
protected {ModuleName}Service $service,
) {}
/**
* List credentials.
*/
public function index()
{
if (request()->ajax()) {
return app(CredentialDataGrid::class)->toJson();
}
return view('{module-name}::credentials.index');
}
/**
* Create form.
*/
public function create()
{
return view('{module-name}::credentials.create');
}
/**
* Store a new credential.
*/
public function store(CredentialForm $request): JsonResponse
{
$credential = $this->credentialRepository->create($request->validated());
return new JsonResponse([
'redirect_url' => route('{module-slug}.credentials.index'),
'message' => trans('{module-name}::app.credentials.create-success'),
]);
}
/**
* Edit form.
*/
public function edit(int $id)
{
$credential = $this->credentialRepository->findOrFail($id);
return view('{module-name}::credentials.edit', compact('credential'));
}
/**
* Update an existing credential.
*/
public function update(CredentialForm $request, int $id): JsonResponse
{
$data = $request->validated();
// Don't overwrite secret if left blank on edit
if (empty($data['consumerSecret'])) {
unset($data['consumerSecret']);
}
$this->credentialRepository->update($data, $id);
return new JsonResponse([
'redirect_url' => route('{module-slug}.credentials.index'),
'message' => trans('{module-name}::app.credentials.update-success'),
]);
}
/**
* Delete a credential.
*/
public function destroy(int $id): JsonResponse
{
$this->credentialRepository->delete($id);
return new JsonResponse([
'message' => trans('{module-name}::app.credentials.delete-success'),
]);
}
/**
* Test the API connection for a credential.
*/
public function testConnection(): JsonResponse
{
$validated = request()->validate([
'apiUrl' => 'required|url',
'consumerKey' => 'required|string',
'consumerSecret' => 'required|string',
]);
try {
$connected = $this->service->testConnection(
$validated['apiUrl'],
$validated['consumerKey'],
$validated['consumerSecret'],
);
if ($connected) {
return new JsonResponse(['message' => trans('{module-name}::app.credentials.test-success')]);
}
return new JsonResponse(
['error' => trans('{module-name}::app.credentials.test-failed')],
422
);
} catch (\Throwable $e) {
return new JsonResponse(['error' => $e->getMessage()], 422);
}
}
}
7. Service Class
A service class wraps all external API calls so controllers stay thin.
<?php
// src/Services/{ModuleName}Service.php
namespace Webkul\{ModuleName}\Services;
use Webkul\{ModuleName}\Http\Client\ApiClient;
use Webkul\{ModuleName}\Models\Credential;
class {ModuleName}Service
{
protected ApiClient $client;
public function __construct(ApiClient $client)
{
$this->client = $client;
}
/**
* Test if the API credentials are valid.
*/
public function testConnection(string $apiUrl, string $key, string $secret): bool
{
$this->client->configure($apiUrl, $key, $secret);
$response = $this->client->get('system_status');
return isset($response['environment']);
}
/**
* Set client from a stored credential.
*/
public function useCredential(Credential $credential): static
{
$this->client->configure(
$credential->apiUrl,
$credential->consumerKey,
$credential->consumerSecret,
);
return $this;
}
}
8. Credential Presenter (HistoryControl)
<?php
// src/Presenters/CredentialPresenter.php
namespace Webkul\{ModuleName}\Presenters;
use Webkul\HistoryControl\Presenters\BasePresenter;
class CredentialPresenter extends BasePresenter
{
/**
* Label shown on history timeline.
*/
public function getTitle(): string
{
return trans('{module-name}::app.credentials.title');
}
}
9. Checklist
- Table name uses DB_PREFIX (auto-added)
- Migration in
Database/Migration/folder - Model uses
HistoryTrait+PresentableHistoryInterface - Model has
$auditExcludefor secret fields (noCrypt::encryptString) - Model has
extrasJSON column with'extras' => 'array'cast - Contract interface exists in
src/Contracts/ -
ModuleServiceProviderlists the model in$models[] - Dedicated
Http/Requests/CredentialForm.phpused (not inline validate) - Controller returns
JsonResponsewithredirect_url - Edit flow keeps existing secret when field left blank
- Service class created for API test connection logic
- Presenter created for history display
@http: cURL HTTP Client
Overview
Unopim connectors use native cURL for HTTP communication — not Guzzle,
not Laravel's Http facade. This matches the production WooCommerce connector
reference (ApiClient.php / BasicAuth.php).
Key rules:
- Use
curl_init()/curl_setopt_array()/curl_exec()/curl_close() - Always check
curl_errno()after exec - Auth classes set cURL options on the handle — not HTTP headers separately
- An
ApiClientclass wraps all HTTP methods (get/post/put/delete) - A
Serviceclass usesApiClient— controllers never callApiClientdirectly
Admin UI compatibility rule:
- If this skill also generates admin Blade setup or credential forms, use UnoPim form components (
x-admin::form.control-group,.label,.control,.error) instead of raw HTML controls.
1. Auth: BasicAuth
Used by WooCommerce (consumer key + secret as HTTP Basic).
<?php
// src/Http/Client/BasicAuth.php
namespace Webkul\{ModuleName}\Http\Client;
class BasicAuth
{
/**
* @param string $key Consumer key / API key
* @param string $secret Consumer secret / API secret
*/
public function __construct(
protected string $key,
protected string $secret,
) {}
/**
* Apply Basic Auth credentials to a cURL handle.
*
* @param resource|\CurlHandle $ch
*/
public function apply($ch): void
{
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, $this->key . ':' . $this->secret);
}
}
2. Auth: BearerToken (OAuth / JWT)
<?php
// src/Http/Client/BearerToken.php
namespace Webkul\{ModuleName}\Http\Client;
class BearerToken
{
public function __construct(protected string $token) {}
/**
* Apply Bearer token to a cURL handle.
*
* @param resource|\CurlHandle $ch
*/
public function apply($ch): void
{
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $this->token,
'Content-Type: application/json',
'Accept: application/json',
]);
}
}
3. ApiClient
<?php
// src/Http/Client/ApiClient.php
namespace Webkul\{ModuleName}\Http\Client;
use Webkul\{ModuleName}\Exceptions\ApiException;
class ApiClient
{
protected string $baseUrl = '';
protected BasicAuth|BearerToken|null $auth = null;
/**
* Configure the client for a specific credential.
*/
public function configure(string $baseUrl, string $key, string $secret): static
{
$this->baseUrl = rtrim($baseUrl, '/');
$this->auth = new BasicAuth($key, $secret);
return $this;
}
/**
* Configure with a Bearer token (e.g. after OAuth exchange).
*/
public function configureWithToken(string $baseUrl, string $token): static
{
$this->baseUrl = rtrim($baseUrl, '/');
$this->auth = new BearerToken($token);
return $this;
}
/**
* Build a full API URL from an endpoint path.
*/
public function buildApiUrl(string $endpoint): string
{
// Subclasses can override to add a version prefix, e.g. /wp-json/wc/v3/
return $this->baseUrl . '/' . ltrim($endpoint, '/');
}
/**
* Issue a GET request.
*
* @param array<string,mixed> $params Query parameters
* @return array<mixed>
*/
public function get(string $endpoint, array $params = []): array
{
$url = $this->buildApiUrl($endpoint);
if ($params) {
$url .= '?' . http_build_query($params);
}
return $this->execute('GET', $url);
}
/**
* Issue a POST request.
*
* @param array<mixed> $data JSON body
* @return array<mixed>
*/
public function post(string $endpoint, array $data = []): array
{
return $this->execute('POST', $this->buildApiUrl($endpoint), $data);
}
/**
* Issue a PUT request.
*
* @param array<mixed> $data
* @return array<mixed>
*/
public function put(string $endpoint, array $data = []): array
{
return $this->execute('PUT', $this->buildApiUrl($endpoint), $data);
}
/**
* Issue a DELETE request.
*
* @return array<mixed>
*/
public function delete(string $endpoint): array
{
return $this->execute('DELETE', $this->buildApiUrl($endpoint));
}
/**
* Execute a cURL request.
*
* @param array<mixed>|null $data
* @return array<mixed>
*
* @throws ApiException
*/
protected function execute(string $method, string $url, ?array $data = null): array
{
$ch = curl_init();
$options = [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Accept: application/json',
],
];
match ($method) {
'POST' => $options[CURLOPT_POST] = true,
'PUT' => $options[CURLOPT_CUSTOMREQUEST] = 'PUT',
'DELETE' => $options[CURLOPT_CUSTOMREQUEST] = 'DELETE',
default => null,
};
if ($data !== null) {
$options[CURLOPT_POSTFIELDS] = json_encode($data);
}
curl_setopt_array($ch, $options);
// Apply auth strategy
if ($this->auth) {
$this->auth->apply($ch);
}
$response = curl_exec($ch);
$errno = curl_errno($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($errno) {
throw new ApiException("cURL error [{$errno}]: " . curl_strerror($errno));
}
if ($httpCode >= 400) {
$body = json_decode($response ?: '', true) ?? [];
$msg = $body['message'] ?? "HTTP {$httpCode} from {$url}";
throw new ApiException($msg, $httpCode);
}
return json_decode($response ?: '[]', true) ?? [];
}
}
4. WooCommerce ApiClient (Subclass Example)
For WooCommerce REST API v3, the base URL includes the WP REST prefix:
<?php
// src/Http/Client/WooCommerceApiClient.php
namespace Webkul\WooCommerce\Http\Client;
class WooCommerceApiClient extends ApiClient
{
/**
* WooCommerce API uses /wp-json/wc/v3/ prefix.
*/
public function buildApiUrl(string $endpoint): string
{
return $this->baseUrl . '/wp-json/wc/v3/' . ltrim($endpoint, '/');
}
}
5. ApiException
<?php
// src/Exceptions/ApiException.php
namespace Webkul\{ModuleName}\Exceptions;
use RuntimeException;
class ApiException extends RuntimeException
{
public function __construct(
string $message,
int $code = 0,
?\Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}
6. OAuth Token Client (for token-based APIs like Shopify)
<?php
// src/Http/Client/OAuthClient.php
namespace Webkul\{ModuleName}\Http\Client;
use Webkul\{ModuleName}\Exceptions\ApiException;
class OAuthClient
{
/**
* Exchange authorization code for an access token.
*
* @return array{access_token: string, scope: string}
* @throws ApiException
*/
public function exchangeCode(
string $shopDomain,
string $clientId,
string $clientSecret,
string $code
): array {
$url = "https://{$shopDomain}/admin/oauth/access_token";
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode([
'client_id' => $clientId,
'client_secret' => $clientSecret,
'code' => $code,
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);
$response = curl_exec($ch);
$errno = curl_errno($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($errno) {
throw new ApiException('OAuth exchange cURL error: ' . curl_strerror($errno));
}
$data = json_decode($response ?: '', true) ?? [];
if ($httpCode !== 200 || empty($data['access_token'])) {
throw new ApiException('OAuth token exchange failed: ' . ($data['error'] ?? 'unknown'));
}
return $data;
}
}
7. Service Provider Binding
Register ApiClient in the module ServiceProvider so it can be injected:
// In {ModuleName}ServiceProvider::register()
$this->app->singleton(\Webkul\{ModuleName}\Http\Client\ApiClient::class);
$this->app->singleton(\Webkul\{ModuleName}\Services\{ModuleName}Service::class);
8. Usage in Service Layer
<?php
// src/Services/{ModuleName}Service.php
namespace Webkul\{ModuleName}\Services;
use Webkul\{ModuleName}\Http\Client\ApiClient;
use Webkul\{ModuleName}\Models\Credential;
class {ModuleName}Service
{
public function __construct(protected ApiClient $client) {}
public function testConnection(string $apiUrl, string $key, string $secret): bool
{
$this->client->configure($apiUrl, $key, $secret);
$response = $this->client->get('system_status');
return isset($response['environment']);
}
public function useCredential(Credential $credential): static
{
$this->client->configure(
$credential->apiUrl,
$credential->consumerKey,
$credential->consumerSecret,
);
return $this;
}
}
9. Checklist
-
ApiClientuses cURL (no Guzzle, no Laravel Http facade) - Auth strategies (
BasicAuth,BearerToken) are separate classes - Auth classes call
curl_setopt()directly on the handle - Always call
curl_errno()aftercurl_exec() - Always call
curl_close()after use - HTTP errors (4xx/5xx) throw
ApiExceptionwith code -
buildApiUrl()is overridable in subclasses for version-prefix APIs -
ApiClientandServiceregistered as singletons in ServiceProvider - Controllers never use
ApiClientdirectly — always via Service - OAuth flow uses separate
OAuthClientclass
@mapping: Attribute Export Mapping
You are a senior Unopim backend engineer.
This skill applies when:
- Creating module Attribute Mapping UI
- Creating module mapping controller
- Removing Custom Field Mapping tab
- Restricting mapping fields to module structure
- Implementing mapping history
1. Allowed Tabs (STRICT)
Admin UI Rule (CRITICAL)
When generating or updating admin Blade forms, always use UnoPim Blade components.
- Use
x-admin::form.control-groupwrappers. - Use
x-admin::form.control-group.labelfor labels. - Use
x-admin::form.control-group.controlfor inputs/selects/textareas. - Use
x-admin::form.control-group.errorfor validation errors. - Do not generate raw
<select>,<input>,<textarea>, or<label>controls when a component equivalent exists. - Keep all user-facing text in translation keys.
For select dropdowns, use type="select" with :options="json_encode(...)", track-by, label-by, and Vue @input event binding.
module Export Mapping must contain ONLY:
- ✅ Attribute Mapping
- ✅ History
❌ Remove Custom Fields Mapping
❌ Remove Other Mapping
❌ Do NOT create extra tabs
UI must match:
Attribute Mapping | History
2. Route Structure (STRICT UNOPIM STANDARD)
Must follow config('app.admin_url') with middleware => ['admin'].
(The web middleware is already applied in the ServiceProvider's
Route::middleware('web')->group(...) — do NOT repeat it in routes.)
Route::group(['middleware' => ['admin'], 'prefix' => config('app.admin_url')], function () {
Route::prefix('module')->group(function () {
Route::prefix('export-mapping')->group(function () {
Route::controller(AttributeMappingController::class)
->prefix('attribute-mapping')
->group(function () {
Route::get('', 'index')
->name('module.export_mappings.attribute_mapping.index');
Route::post('save', 'store')
->name('module.export_mappings.attribute_mapping.store');
});
Route::controller(MappingHistoryController::class)
->group(function () {
Route::get('history', 'index')
->name('module.export_mappings.history');
});
});
});
});
3. module Attribute Mapping Fields (STRICT FIELD SET)
| module Field | Required Unopim Attribute Type |
|---|---|
| EAN | text |
| Product Code | text OR number |
| Vendor (Brand) | simple select OR text |
| Name | text |
| Title | text |
| Description | textarea |
| Short Description | textarea |
| Summary Description | textarea |
| Short Summary Description | textarea |
| Pictures | image (multiple selection allowed) |
❌ Do NOT allow unsupported attribute types
❌ Do NOT allow dynamic field injection
❌ Do NOT allow additional fields
4. Attribute Type Validation Rule (MANDATORY)
$allowedTypes = [
'ean' => ['text'],
'product_code' => ['text', 'number'],
'vendor' => ['select', 'text'],
'name' => ['text'],
'title' => ['text'],
'description' => ['textarea'],
'short_description' => ['textarea'],
'summary_description' => ['textarea'],
'short_summary_description' => ['textarea'],
'pictures' => ['image'],
];
If mismatch → throw validation error with field name.
5. Pictures Attribute Rule
- Allow selecting image-type attributes only
- Allow multiple images (multi-select)
- Store as JSON array
- Validate attribute type = image on save
6. Database Structure
Table: module_attribute_mappings
| Column | Type | Notes |
|---|---|---|
| id | bigInt PK | |
| attribute_code | string | module field key |
| unopim_attribute_id | bigInt FK | |
| default_value | json null | Optional defaults |
| created_at / updated_at | timestamps |
For pictures: store multiple attribute IDs as JSON array.
Table: module_mapping_histories
| Column | Type | Notes |
|---|---|---|
| id | bigInt PK | |
| action_type | string | create / update |
| user_id | bigInt FK | |
| payload | json | Full snapshot of mapping |
| created_at | timestamp |
7. Store Flow
- Validate attribute type compatibility
- Validate all required fields are mapped (EAN, Product Code, Name, Description)
- Store mapping (upsert)
- Save history snapshot
8. Required Mandatory Fields
Must be mapped before saving:
- EAN
- Product Code
- Name
- Description
If any missing → block save with validation error.
9. Controller Requirements
Generate two controllers:
AttributeMappingController— index + storeMappingHistoryController— index only
Rules:
- No DB logic in controllers
- Use repository injection
- Validate types before storing
- Log history on every save
10. UI Rules
Table columns: module Field | Unopim Field | Default Value
- Default value column is optional per field
- Pictures field must render multi-select dropdown (image attributes only)
- Use Unopim admin blade components
11. Security & Validation Rules
- Only admin users via
middleware(['admin'])(NOT['web', 'admin']) - Validate attribute exists in Unopim
- Validate attribute type against allowed set
- Prevent duplicate mappings per attribute_code
- Prevent invalid attribute IDs
- Log errors with
Log::error()
12. Output Requirements
When generating module Export Mapping module, MUST generate:
- Migration:
module_attribute_mappings - Migration:
module_mapping_histories - Model + Contract interface for each table
- Repository for each model
AttributeMappingControllerMappingHistoryController- Validation logic (type-aware)
- Routes file (strict Unopim structure)
- Blade view (exactly 2 tabs)
- ACL entry in
acl.php - Menu entry in
menu.php
FINAL RULE
module export mapping must be:
- Minimal (2 tabs only)
- Strictly validated with type-checking
- Attribute-type restricted
- History-tracked on every change
- Unopim route compliant
- Clean and repository-driven