From 1d9c658af4f42ea2f7735160702fd779f2452f24 Mon Sep 17 00:00:00 2001 From: Navneet Kumar Date: Mon, 10 Aug 2026 22:16:41 +0530 Subject: [PATCH 1/3] docs(3.0): correct the 3.0 documentation against the codebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed every page in src/3.0 against the UnoPim 3.0 source and fixed what did not match. The changes fall into a few groups. Documented software that does not exist: - Removed agentic/mcp-server.md and agentic/extending-mcp.md. There is no Webkul\MCP package, no BaseMcpTool, no ToolRegistry under that namespace and no MCP_* config keys anywhere in the repository. Every surviving reference in the agentic section was rewritten around the interfaces that do exist, which meant reworking index.md and rebuilding five of the eight recipes on real agent tool names. - Removed the `unopim:translate` section; the command is not registered. - Removed thirteen `core()` helpers from advanced/helpers.md (convertPrice, getExchangeRate, states, isCountryRequired, …). They are eCommerce leftovers with no counterpart in Core.php. - Removed the media.images, media.videos and quantity-changer components, which 3.0 deleted, and documented media.image and media.gallery with their real props. Instructions that would fail if followed: - plugins/create-plugin.md registered the service provider in config/app.php's providers array, which Laravel 11 removed. A plugin built from that page never loads. - The Nginx vhosts in web-server-configuration.md and the three OS guides put the static-file rule ahead of the front controller, so /cache/ thumbnails and /p/{uuid}/carrier.svg 404. Added the guards the shipped dockerfiles/nginx.conf already has, plus its PHP-handler hardening. - installation-docker.md advertised admin123 as the Docker password. The seeder generates a random one and writes it to storage/app/admin-credentials.txt. - The worker command omitted the webhooks and publication queues, so deliveries and passport publishing queued up unprocessed. - CACHE_DRIVER and ELASTICSEARCH_PORT are dead variables; the installer expects host:port in ELASTICSEARCH_HOST. - Both installers listed prompts that no longer match Installer.php and omitted the Elasticsearch block entirely. - Repository examples used Input::all(), removed in Laravel 6. Conventions the docs contradicted: - routes.php used ['web', 'admin']; every core route group uses ['admin']. - validation.md taught inline $request->validate(); rewritten FormRequest-first. - The wk_ prefix was stated as fact; DB_PREFIX defaults to empty. - Menu and ACL examples hardcoded English labels. - Migrations were said to live in Database/Migration/ (singular). Missing reference material: - Added api/passports.md, api/association_types.md and api/variant_structures.md. The passport endpoints were reachable only through a dead anchor; the other two arrived with the catalog routes. - Documented swatch upload, configurable-product delete, the delta-sync filters and cursor parameters, and the product associations block. - architecture/packages.md documented FPC, removed in 3.0, and omitted six real packages; the list now matches packages/Webkul exactly. Every artisan command, Webkul class, Blade component, env var, config key, table and route named in the docs was verified against source. --- .vitepress/version-configs/3.0.ts | 9 +- src/3.0/advanced/cli-commands.md | 8 +- src/3.0/advanced/digital-product-passport.md | 4 + .../advanced/elasticsearch-configuration.md | 2 +- src/3.0/advanced/events.md | 10 +- src/3.0/advanced/helpers.md | 105 ------- src/3.0/advanced/index.md | 2 +- src/3.0/advanced/override-core-model.md | 8 +- src/3.0/advanced/queue-management.md | 8 +- src/3.0/advanced/render-event.md | 15 +- src/3.0/advanced/security-practice.md | 4 +- src/3.0/agentic/agent-skills.md | 8 +- src/3.0/agentic/ai-agent.md | 63 ++--- src/3.0/agentic/building-agent-tools.md | 139 +++++---- src/3.0/agentic/building-integrations.md | 48 ++-- src/3.0/agentic/extending-mcp.md | 160 ----------- src/3.0/agentic/index.md | 53 ++-- src/3.0/agentic/magic-ai-platform.md | 21 +- src/3.0/agentic/mcp-server.md | 266 ------------------ src/3.0/agentic/recipes.md | 84 +++--- src/3.0/api/association_types.md | 153 ++++++++++ src/3.0/api/attribute.md | 2 +- src/3.0/api/attribute_families.md | 14 + src/3.0/api/attribute_groups.md | 14 +- src/3.0/api/attribute_options.md | 18 ++ src/3.0/api/authenticate.md | 12 + src/3.0/api/category.md | 9 +- src/3.0/api/category_field_options.md | 2 + src/3.0/api/category_fields.md | 9 +- src/3.0/api/channel.md | 7 + src/3.0/api/configurable_products.md | 29 +- src/3.0/api/configuration.md | 16 +- src/3.0/api/currency.md | 2 +- src/3.0/api/index.md | 4 + src/3.0/api/locales.md | 2 +- src/3.0/api/media.md | 48 +++- src/3.0/api/migrating-your-client.md | 250 ++++++++++++++++ src/3.0/api/passports.md | 139 +++++++++ src/3.0/api/product.md | 52 +++- src/3.0/api/variant_structures.md | 122 ++++++++ src/3.0/api/whats-new-v3.md | 8 +- src/3.0/architecture/frontend.md | 2 +- src/3.0/architecture/index.md | 4 +- src/3.0/architecture/packages.md | 62 ++-- src/3.0/architecture/repository-pattern.md | 2 +- src/3.0/introduction/index.md | 4 +- src/3.0/introduction/installation-centos.md | 24 +- src/3.0/introduction/installation-debian.md | 24 +- src/3.0/introduction/installation-docker.md | 15 +- src/3.0/introduction/installation-ubuntu.md | 22 +- .../installation-with-postgresql.md | 57 ++-- src/3.0/introduction/installation.md | 41 +-- src/3.0/introduction/queue-scheduler-setup.md | 15 +- src/3.0/introduction/requirements.md | 26 +- .../introduction/web-server-configuration.md | 81 +++++- src/3.0/packages/add-menu-in-admin.md | 4 +- src/3.0/packages/blade-components.md | 94 ++++--- src/3.0/packages/bundling-assets.md | 21 +- src/3.0/packages/configurable-associations.md | 2 +- src/3.0/packages/controllers.md | 7 +- src/3.0/packages/create-acl.md | 10 +- src/3.0/packages/create-migrations.md | 2 +- src/3.0/packages/create-models.md | 6 +- src/3.0/packages/data-transfer.md | 26 +- src/3.0/packages/datagrid.md | 28 +- src/3.0/packages/history.md | 10 +- src/3.0/packages/index.md | 7 +- src/3.0/packages/layouts.md | 87 ++++-- src/3.0/packages/localization.md | 15 + src/3.0/packages/routes.md | 10 +- .../store-data-through-repositories.md | 12 +- src/3.0/packages/swatch-types.md | 6 +- src/3.0/packages/validation.md | 222 ++++++--------- src/3.0/packages/views.md | 35 +-- src/3.0/plugins/add-side-menu.md | 4 +- src/3.0/plugins/create-export-profile.md | 14 +- src/3.0/plugins/create-import-profile.md | 12 +- src/3.0/plugins/create-plugin.md | 20 +- src/3.0/plugins/index.md | 4 +- src/3.0/prologue/contribution-guide.md | 41 ++- src/3.0/prologue/index.md | 13 +- src/3.0/prologue/patch-update.md | 63 +++-- src/3.0/prologue/release-notes.md | 16 +- src/3.0/prologue/upgrade-guide.md | 12 +- 84 files changed, 1864 insertions(+), 1247 deletions(-) delete mode 100644 src/3.0/agentic/extending-mcp.md delete mode 100644 src/3.0/agentic/mcp-server.md create mode 100644 src/3.0/api/association_types.md create mode 100644 src/3.0/api/migrating-your-client.md create mode 100644 src/3.0/api/passports.md create mode 100644 src/3.0/api/variant_structures.md diff --git a/.vitepress/version-configs/3.0.ts b/.vitepress/version-configs/3.0.ts index 96c67187..e068a9d5 100644 --- a/.vitepress/version-configs/3.0.ts +++ b/.vitepress/version-configs/3.0.ts @@ -121,10 +121,8 @@ export default [ ['agentic/ai-agent', 'AI Agent Integration'], ['agentic/magic-ai-platform', 'MagicAI Platform Management'], ['agentic/agent-skills', 'Agentic Skills'], - ['agentic/mcp-server', 'MCP Server'], ['agentic/building-integrations', 'Building an Integration with AI'], ['agentic/building-agent-tools', 'Building Custom Agent Tools'], - ['agentic/extending-mcp', 'Extending the MCP Bridge'], ['agentic/recipes', 'Agentic Recipes'], ]) }, @@ -134,6 +132,7 @@ export default [ collapsed: false, items: setVersionPrefix([ ['api/whats-new-v3', "What's New in v3.0"], + ['api/migrating-your-client', 'Migrating an API Client'], ['api/configuration', 'Configuration'], ['api/authenticate', 'Authentication'], ['api/attribute', 'Attribute'], @@ -146,9 +145,15 @@ export default [ ['api/product', 'Product'], ['api/configurable_products', 'Configurable Products'], ['api/media', 'Media'], + ['api/association_types', 'Association Types'], + ['api/variant_structures', 'Variant Structures'], + ['api/passports', 'Digital Product Passports'], ['api/channel', 'Channel'], ['api/locales', 'Locales'], ['api/currency', 'Currency'], + ['api/explanation', 'Response Structure'], + ['api/getting-started-with-the-api', 'Getting Started'], + ['api/postman_collection', 'Postman Collection'], ]) } ] diff --git a/src/3.0/advanced/cli-commands.md b/src/3.0/advanced/cli-commands.md index f217a6ed..921ea69d 100644 --- a/src/3.0/advanced/cli-commands.md +++ b/src/3.0/advanced/cli-commands.md @@ -11,7 +11,7 @@ UnoPim provides a set of Artisan commands for managing your PIM installation. Th | `php artisan unopim:install:demo-data` | Seed the sample catalog (`--force` to re-seed, `--scale=large` for a 2,000-product performance dataset) | | `php artisan unopim:version` | Display the current UnoPim version | | `php artisan unopim:publish` | Publish UnoPim assets and config (`--force` to overwrite) | -| `php artisan unopim:user:create` | Create an admin user (`--name`, `--email`, password, UI locale, timezone, admin flag) | +| `php artisan unopim:user:create` | Create a user (`--name=`, `--email=`, `--password=`, `--ui_locale=`, `--timezone=`, `--admin`) | | `php artisan unopim:images:purge-unused` | Remove unused images from storage (`--dry-run` to preview) | | `php artisan unopim:translations:check` | Audit translation files across all packages against the `en_US` canonical set (`--locale=`, `--package=`) | @@ -27,7 +27,7 @@ UnoPim provides a set of Artisan commands for managing your PIM installation. Th | Command | Description | |---------|-------------| -| `php artisan unopim:completeness:recalculate` | Recalculate product completeness (`--family=`, `--product=`, or all) | +| `php artisan unopim:completeness:recalculate` | Recalculate product completeness (`--family=`, `--product=`, repeatable `--products=`, or `--all`) | | `php artisan unopim:variants:strip-redundant` | Remove child attribute values that duplicate an inherited ancestor value. Dry-run by default — pass `--apply` to write, `--product=` to scope | | `php artisan unopim:variants:resync` | Rebuild derived data (completeness, search index) for variant subtrees (`--product=`, `--all`) | | `php artisan measurement:recalculate` | Rebuild the stored base value of every product measurement from current family definitions (`--family=`, `--chunk=200`) | @@ -65,7 +65,7 @@ See [Digital Product Passport](digital-product-passport) for the preset config s | Command | Description | |---------|-------------| | `php artisan ai-agent:embeddings:index` | Queue (re)indexing of product embeddings into the AI vector store (`--since=`, `--batch=`) | -| `php artisan ai-agent:quality-monitor` | Scan the catalog for data-quality issues (`--channel=`, `--locale=`) | +| `php artisan ai-agent:quality-monitor` | Scan the catalog for data-quality issues (`--channel=default`, `--locale=en_US`, `--limit=500`) | | `php artisan ai-agent:cleanup` | Clean up temporary AI files (`--days=7`, `--dry-run`) | ## Scheduled Commands @@ -91,7 +91,7 @@ Make sure the scheduler is running: Most heavy work is queued. A production worker should listen on every queue in use: ```bash -php artisan queue:work --queue="system,completeness,publication,default" +php artisan queue:work --queue="system,completeness,publication,webhooks,default" ``` The `publication` queue (new in 3.0) carries all Digital Product Passport publishing, bulk transitions, and view-count aggregation. See [Queue Management](queue-management) for Supervisor configuration. diff --git a/src/3.0/advanced/digital-product-passport.md b/src/3.0/advanced/digital-product-passport.md index a681bc58..a035ecb3 100644 --- a/src/3.0/advanced/digital-product-passport.md +++ b/src/3.0/advanced/digital-product-passport.md @@ -247,3 +247,7 @@ All publishing, bulk transitions and view counting run on the `publication` queu php artisan queue:work --queue=publication ``` ::: + +## REST API + +Passports are fully manageable over the REST API — list publications, read them per SKU, publish, withdraw, reinstate, and redact. See [Digital Product Passports](../api/passports) in the API reference. diff --git a/src/3.0/advanced/elasticsearch-configuration.md b/src/3.0/advanced/elasticsearch-configuration.md index db86e865..2a546878 100644 --- a/src/3.0/advanced/elasticsearch-configuration.md +++ b/src/3.0/advanced/elasticsearch-configuration.md @@ -143,7 +143,7 @@ php artisan unopim:category:index ## Filter Improvements (v2.0.0) -UnoPim v2.0.0 includes several improvements to Elasticsearch filter handling: +UnoPim includes several improvements to Elasticsearch filter handling: - **SKU Filters** — Improved handling for exact and partial SKU matching. - **Text Filters** — Better support for text-based attribute filtering with improved tokenization. diff --git a/src/3.0/advanced/events.md b/src/3.0/advanced/events.md index 83f3280c..5af220b7 100644 --- a/src/3.0/advanced/events.md +++ b/src/3.0/advanced/events.md @@ -11,7 +11,7 @@ In UnoPim, events and listeners are organized in a clear and structured manner: This organization makes it easy to manage and locate the event-driven components of your application. -To learn in detail about Controllers, you can visit the Laravel documentation [here](https://laravel.com/docs/10.x/events). +To learn in detail about Controllers, you can visit the Laravel documentation [here](https://laravel.com/docs/13.x/events). ## Creating an Event Class @@ -51,7 +51,7 @@ class EventServiceProvider extends ServiceProvider { //... - Event::listen('catalog.attribute.create.after', 'Webkul\Catalog\Listeners\Attribute@handleAttributeCreated'); + Event::listen('catalog.attribute.create.after', 'App\Listeners\AttributeListener@handleAttributeCreated'); } } ``` @@ -63,9 +63,9 @@ In UnoPim, events are typically fired before and after the execution of CRUD ope For example, you might have events fired during product creation, updating, or deletion. Here’s an example of firing events before and after saving a product: ```php -namespace Webkul\Catalog\Repositories; +namespace Webkul\Product\Repositories; -use Webkul\Catalog\Contracts\Product; +use Webkul\Product\Contracts\Product; class ProductRepository extends Repository { @@ -199,7 +199,7 @@ Open the `EventServiceProvider.php` file located in the `Providers` directory of Inside the `boot()` method of `EventServiceProvider.php`, use the `Event::listen` method to register your listener. This method takes the event name and a callback function or a class method that will handle the event. ```php -Event::listen('catalog.product.create.after', 'Webkul\Notification\Listeners\Product@createNotification'); +Event::listen('catalog.product.create.after', 'App\Listeners\ProductListener@createNotification'); ``` By registering the listener, you have associated the **`createNotification`** function with the **`catalog.product.create.after`** event. Whenever this event is triggered, the specified function will be executed. diff --git a/src/3.0/advanced/helpers.md b/src/3.0/advanced/helpers.md index fab2e753..41e567ad 100644 --- a/src/3.0/advanced/helpers.md +++ b/src/3.0/advanced/helpers.md @@ -188,41 +188,6 @@ To set the current currency in UnoPim using the `core()->setCurrentCurrency()` m core()->setCurrentCurrency() ``` -### Get current channel's currency model - -```php -core()->getCurrentCurrency() -``` - -- **Get current channel's currency code.** - -To retrieve the current channel's currency model in UnoPim, you should use the `core()->getCurrentCurrency()` method. Here's how you can use it: - -```php -core()->getCurrentCurrencyCode() -``` - -This function call retrieves the currency model of the current channel, allowing you to access attributes such as the currency code, symbol, exchange rates, and other relevant information related to currency management within your pim application. - -### Get exchange rates - -To get exchange rates in UnoPim, you typically need to specify the base currency and the target currency for which you want to retrieve the exchange rate. Here's how you can achieve this: - -```php -core()->getExchangeRate() -``` - -### Converts price. - -The `core()->convertPrice()` function in UnoPim is used to convert a given amount from the base currency to a specified target currency. Here's how you can use it: - -```php -$amount = 100; // Replace with the amount you want to convert -$targetCurrencyCode = 'EUR'; // Replace with the target currency code - -$convertedAmount = core()->convertPrice($amount, $targetCurrencyCode); -``` - ### Converts to base price The `core()->convertToBasePrice()` function in UnoPim is used to convert a given amount from a specified currency (target currency) to the base currency of the application. Here's how you can use it: @@ -273,14 +238,6 @@ This method also give ability to encode the base currency symbol and its optiona core()->formatBasePrice($price, $isEncoded = false) ``` -### Checks if current date of the given channel (in the channel timezone) is within the range - -The `core()->isChannelDateInInterval($dateFrom = null, $dateTo = null)` function in UnoPim checks if the current date of the given channel (considering the channel's timezone) falls within the specified date range. - -```php -core()->isChannelDateInInterval($dateFrom = null, $dateTo = null) -``` - ### Get channel timestamp, timestamp will be builded with channel timezone settings. To retrieve a timestamp that adheres to a specific channel's timezone settings in UnoPim, you typically use the `core()->channelTimeStamp($channel)` function. Here's how you can implement it: @@ -325,42 +282,6 @@ To retrieve all countries in UnoPim, you can use the `core()->countries()` funct core()->countries() ``` -### Get country name by code - -To get the country name by its ISO 3166-1 alpha-2 code in UnoPim, you can use the `core()->country_name($code)` function. Here's how you can use it - -```php -core()->country_name($code) -``` - -This function retrieves the full name of the country based on its ISO 3166-1 alpha-2 code ($code). - -### Retrieve all country states - -To retrieve all states (or provinces) of a specific country in UnoPim, you can use the `core()->states($countryCode)` function. Here's how you can use it - -```php -core()->states($countryCode) -``` - -This function returns a collection of state objects for the specified country - -### Retrieve all grouped states by country code. - -In UnoPim, to retrieve all states grouped by country code, you can use the `core()->groupedStatesByCountries()` function. This function organizes states or provinces by their respective countries. Here's how you can use it: - -```php -core()->groupedStatesByCountries() -``` - -### Get states by country code. - -To retrieve states (or provinces) by country code in UnoPim, you can use the `core()->findStateByCountryCode($countryCode, $stateCode = null)` function. Here’s how you can use it - -```php -core()->findStateByCountryCode($countryCode = null, $stateCode = null) -``` - ### Get guest customer group In UnoPim, to get the guest customer group, you can use the `core()->getGuestCustomerGroup()` function. Here's how you can use it @@ -371,32 +292,6 @@ core()->getGuestCustomerGroup() This function retrieves the guest customer group configured in your UnoPim application. It returns an object representing the guest customer group -### Is country required - -In UnoPim, to check if a country selection is required (typically in address forms or checkout processes), you can use the `core()->isCountryRequired()` function. Here's how you can use it: - -```php -core()->isCountryRequired() -``` - -This function returns a boolean (true or false) indicating whether the country selection is mandatory - -### Is state required - -In UnoPim, to check if a state or province selection is required (typically in address forms or checkout processes), you can use the `core()->isStateRequired()` function. Here's how you can use it: - -```php -core()->isStateRequired() -``` - -### Is postcode required. - -This function returns a boolean (true or false) indicating whether the postcode (or ZIP code) selection is mandatory. - -```php -core()->isPostCodeRequired() -``` - ### Week range In UnoPim, there isn't a specific `core()->xWeekRange()` function predefined. However, if you need to calculate a date range based on a given date and the number of weeks before or after that date, you can achieve this using PHP's DateTime and DateInterval classes. Here’s how you can calculate a week range diff --git a/src/3.0/advanced/index.md b/src/3.0/advanced/index.md index d14999e9..9d9bc8ff 100644 --- a/src/3.0/advanced/index.md +++ b/src/3.0/advanced/index.md @@ -17,5 +17,5 @@ UnoPim includes a comprehensive set of helper functions that simplify common dev Sometimes, you may need to modify or extend the default behavior of UnoPim's core models to accommodate your specific business requirements. We will demonstrate how to override core models effectively, enabling you to customize the behavior of UnoPim without modifying the underlying codebase. ::: tip -Looking for the AI Agent and MagicAI platform documentation? Those topics now live in the dedicated [Agentic Development](../agentic/) section, alongside the Agentic Skills and MCP Server guides. +Looking for the AI Agent and MagicAI platform documentation? Those topics now live in the dedicated [Agentic Development](../agentic/) section, alongside the Agentic Skills guides. ::: diff --git a/src/3.0/advanced/override-core-model.md b/src/3.0/advanced/override-core-model.md index 5d5906f7..5be74129 100644 --- a/src/3.0/advanced/override-core-model.md +++ b/src/3.0/advanced/override-core-model.md @@ -43,14 +43,14 @@ class ExampleServiceProvider extends ServiceProvider //... $this->app->concord->registerModel( - \Webkul\Product\Contracts\Product::class, \App\Http\Product::class + \Webkul\Product\Contracts\Product::class, \App\Models\Product::class ); } } ``` - Replace `\Webkul\Product\Contracts\Product::class` with the interface you wish to override. -- Replace `\App\Http\Product::class` with the path to your custom model class that extends the core model you are overriding. +- Replace `\App\Models\Product::class` with your custom model class, which must extend the core model you are overriding. ### Implement the Custom Model Class @@ -59,7 +59,7 @@ Your custom model class (Product in this example) should extend the base core mo ```php field('home_page_content') - ->with(['sliderData' => $sliderData])->render() !!} +
+ {{-- the page's own markup --}} +
{!! view_render_event('unopim.admin.layout.content.after') !!} @endsection ``` -In this example `unopim.admin.layout.content.before` and `unopim.admin.layout.content.after` are custom event names that denote where content should be injected before and after the home_page_content section, respectively. +Here `unopim.admin.layout.content.before` and `unopim.admin.layout.content.after` are event names marking where content may be injected, before and after the page content. + +The helper also accepts a second argument, passed through to every listener — core views use this to hand over the record being rendered: + +```blade +{!! view_render_event('unopim.admin.system_settings.edit.'.$entry['key'].'.before', ['entry' => $entry]) !!} +``` ### Listening to Events diff --git a/src/3.0/advanced/security-practice.md b/src/3.0/advanced/security-practice.md index 02d8c1f7..09ab0cc4 100644 --- a/src/3.0/advanced/security-practice.md +++ b/src/3.0/advanced/security-practice.md @@ -123,7 +123,7 @@ Follow these guidelines to enhance the security of your UnoPim instance and prot --- ## 10. API Security Improvements (v2.0.0) -UnoPim v2.0.0 includes significant API security hardening: +UnoPim includes significant API security hardening: - **Full ACL enforcement on all API routes**: All 48 API routes now have proper ACL authorization checks. 15 previously unprotected routes have been fixed to require appropriate permissions. - **ACL authorization on AI Agent tools**: All 32 AI Agent tools enforce permission checks via the `ChecksPermission` trait, ensuring that AI-driven operations respect the same role-based access controls as manual actions. @@ -135,7 +135,7 @@ UnoPim v2.0.0 includes significant API security hardening: --- ## 11. Security Enhancements (v2.1.0) -UnoPim v2.1.0 adds further hardening on top of the v2.0.0 API improvements: +Further hardening on top of those API improvements: ### IP-based debug filtering diff --git a/src/3.0/agentic/agent-skills.md b/src/3.0/agentic/agent-skills.md index a004d3e2..cf6fa0e1 100644 --- a/src/3.0/agentic/agent-skills.md +++ b/src/3.0/agentic/agent-skills.md @@ -120,8 +120,8 @@ When your request matches a skill's description — for example, "add a credenti Alongside the skills, the repository ships an `AGENTS.md` that establishes the foundation rules every agent must follow when touching an UnoPim codebase. The critical conventions: -- **Table prefix** — all tables use the `wk_` prefix (e.g. `wk_products`); reference unprefixed names in `DB::table()` (Laravel adds the prefix). -- **Migration folder** — package migrations live in `Database/Migration/` (singular, no `s`). +- **Table prefix** — tables are declared unprefixed (`products`, `categories`). An installation may opt into a prefix with `DB_PREFIX`, which defaults to empty, so always reference the unprefixed name in `DB::table()` and let the query builder apply the prefix. +- **Migration folder** — package migrations live in `src/Database/Migrations/`. - **Route middleware** — use `['admin']` only, never `['web', 'admin']`. - **Models with history** — implement `PresentableHistoryInterface` and use `HistoryTrait`. - **Controllers return JSON** — store/update/delete return `JsonResponse` with `redirect_url` and `message`. @@ -133,7 +133,7 @@ Alongside the skills, the repository ships an `AGENTS.md` that establishes the f - **ACL config** — flat arrays, no nested `children`. - **No hardcoded strings** — all user-facing text via `trans('package::file.key')`, propagated to all supported locales. -The file also pins the mandatory development pipeline: write Pest tests → run Laravel Pint → run Playwright E2E for UI flows → verify translations with `php artisan unopim:translations:check`. +The file also pins the mandatory development pipeline: write Pest tests → run Laravel Pint → run Larastan → run Playwright E2E for UI flows → verify translations with `php artisan unopim:translations:check`. --- @@ -165,5 +165,5 @@ Step-by-step, opinionated guidance the agent should follow ... Keep skills **focused and specific**: a sharp `description` ensures the agent activates the skill at the right moment, and a focused body keeps the loaded context small. Large skills can be split into `@`-tagged reference sections that the agent pulls in only when needed. ::: tip -The same `SKILL.md` format is reused by the [MCP Server](./mcp-server.html) — drop a `SKILL.md` into `.ai/skills/` and the MCP bridge auto-registers it as an executable tool, no code required. +The same `SKILL.md` format works for skills you write yourself — drop a new directory alongside the others and your agent picks it up, no code required. ::: diff --git a/src/3.0/agentic/ai-agent.md b/src/3.0/agentic/ai-agent.md index 71f16f62..19f2abd1 100644 --- a/src/3.0/agentic/ai-agent.md +++ b/src/3.0/agentic/ai-agent.md @@ -2,7 +2,7 @@ ## Introduction -UnoPim v2.0.0 introduced the **AI Agent** — a conversational interface for managing your product catalog using natural language. Built on [prism-php/prism](https://github.com/prism-php/prism) for multi-provider AI tool calling, the agent provides 38+ PIM tools that let you search, create, update, delete, bulk edit, export, categorize, generate content and images, plan multi-step workflows, and more — all from a single chat widget. +UnoPim ships an **AI Agent** — a conversational interface for managing your product catalog using natural language. Built on [`laravel/ai`](https://github.com/laravel/ai) for multi-provider AI tool calling, the agent provides 35 PIM tools that let you search, create, update, delete, bulk edit, export, categorize, generate content and images, plan multi-step workflows, and more — all from a single chat widget. The agent is accessible from any page in the admin panel via the floating chat widget. It supports Server-Sent Events (SSE) streaming for real-time progress feedback and persists conversation history in the database. @@ -16,12 +16,12 @@ The AI Agent package (`Webkul\AiAgent`) follows UnoPim's modular Concord archite packages/Webkul/AiAgent/ ├── src/ │ ├── Chat/ # Agent runner, tool registry, tools, context -│ │ ├── AgentRunner.php # Prism-based orchestration loop +│ │ ├── AgentRunner.php # laravel/ai orchestration loop │ │ ├── ChatContext.php # Immutable request-scoped DTO │ │ ├── ToolRegistry.php # Singleton tool collection │ │ ├── Contracts/PimTool.php # Interface all tools implement │ │ ├── Concerns/ # Reusable traits (ACL, approval) -│ │ └── Tools/ # 38+ individual tool classes +│ │ └── Tools/ # 35 individual tool classes │ ├── Http/Controllers/ # Chat, Conversation, Approval, Dashboard │ ├── Jobs/ # Auto-enrichment, translation, batch jobs │ ├── Console/Commands/ # Quality monitor, temp cleanup @@ -30,7 +30,7 @@ packages/Webkul/AiAgent/ │ ├── Repositories/ # Data access layer │ ├── Services/ # ProductWriterService, EnrichmentService, etc. │ └── Providers/ # Service provider, tool registration -├── Database/Migration/ # Database schema +├── Database/Migration/ # Database schema (legacy singular folder in this package) └── Routes/ # Admin routes ``` @@ -39,9 +39,9 @@ packages/Webkul/AiAgent/ 1. User sends a message via the chat widget (POST to `/chat/stream`). 2. `ChatController` builds an immutable `ChatContext` DTO from the request. 3. The session lock is released before the LLM call to prevent blocking other requests. -4. `AgentRunner` constructs a Prism request with all registered tools and the system prompt. +4. `AgentRunner` constructs a `laravel/ai` request with all registered tools and the system prompt. 5. The LLM autonomously decides which tools to call based on user intent. -6. Prism executes each tool, feeds results back, and iterates until a final text response. +6. `laravel/ai` executes each tool, feeds results back, and iterates until a final text response. 7. SSE events stream progress (tool invocations) and the final response to the client. 8. Token usage is recorded for budget tracking. @@ -96,13 +96,13 @@ Every tool must implement this contract: ```php namespace Webkul\AiAgent\Chat\Contracts; -use Prism\Prism\Tool; +use Laravel\Ai\Contracts\Tool; use Webkul\AiAgent\Chat\ChatContext; interface PimTool { /** - * Return a configured Prism Tool instance. + * Return a configured laravel/ai Tool instance. */ public function register(ChatContext $context): Tool; } @@ -110,7 +110,7 @@ interface PimTool ### Available Tools -The agent ships with 38+ tools organized by category: +The agent ships 35 tools organized by category: | Category | Tools | |----------|-------| @@ -188,7 +188,7 @@ To add a new tool, create a class that implements `PimTool` and register it with ```php namespace App\AiAgent\Tools; -use Prism\Prism\Tool; +use Laravel\Ai\Contracts\Tool; use Webkul\AiAgent\Chat\ChatContext; use Webkul\AiAgent\Chat\Concerns\ChecksPermission; use Webkul\AiAgent\Chat\Contracts\PimTool; @@ -409,32 +409,6 @@ TranslateProductValuesJob::dispatch( --- -## AI Translation Command - -The `unopim:translate` Artisan command provides a CLI alternative for bulk translating product attribute values across all configured locales using AI providers. - -### Running the Command - -```bash -php artisan unopim:translate -``` - -### When to Use - -While the Auto-Translation feature (above) handles translations automatically when products are created or updated through the agent, the `unopim:translate` command is designed for: - -- **Retroactive translation** — Translate existing products that were created before auto-translation was enabled. -- **Bulk operations** — Translate large batches of products without going through the chat interface. -- **CI/CD pipelines** — Integrate translation into automated deployment or data migration workflows. - -The command uses the same AI provider configured under **Magic AI** settings and translates the same locale-dependent fields (`name`, `description`, `meta_title`, `meta_description`, `meta_keywords`). - -::: tip -This command is especially useful after a large CSV import where products were loaded in a single locale and need to be translated to all active locales in bulk. -::: - ---- - ## Content Feedback Loop The `RateContent` tool captures user feedback on AI-generated content quality. This creates a feedback loop that improves future content generation. @@ -698,7 +672,7 @@ Task planning and decomposition. ### ACL on Every Tool -All 38+ tools check user permissions via the `ChecksPermission` trait before executing write operations. Read-only tools like `SearchProducts` check `catalog.products`, while write tools check specific permissions like `catalog.products.create` or `catalog.products.edit`. +All tools check user permissions via the `ChecksPermission` trait before executing write operations. Read-only tools like `SearchProducts` check `catalog.products`, while write tools check specific permissions like `catalog.products.create` or `catalog.products.edit`. ### Rate Limiting @@ -830,11 +804,9 @@ eventSource.addEventListener('done', (e) => { ## Friendly Error Messages -::: info Added in v2.1.0 -The `PrismErrorResolver` (`Webkul\AiAgent\Chat\PrismErrorResolver`) translates raw provider and Prism exceptions into clear, user-friendly messages before they reach the chat widget. -::: +The `AiErrorResolver` (`Webkul\AiAgent\Chat\AiErrorResolver`) translates raw provider exceptions into clear, user-friendly messages before they reach the chat widget. -When a chat request fails, `AgentRunner` catches the exception and passes it to `PrismErrorResolver::resolve()`. This is the single public method on the class, and it returns an array with three keys: +When a chat request fails, `AgentRunner` catches the exception and passes it to `AiErrorResolver::resolve()`. This is the single public method on the class, and it returns an array with three keys: | Key | Description | |-----|-------------| @@ -847,12 +819,13 @@ The resolver maps common failure types to meaningful responses: | Exception | HTTP Status | Message | |-----------|-------------|---------| | `DecryptException` | 422 | The stored API key is corrupted and must be re-entered (typically after an `APP_KEY` change) | -| `PrismRateLimitedException` | 429 | The provider rate limit was hit, including retry timing when the provider supplies it | -| `PrismProviderOverloadedException` | 503 | The provider is temporarily overloaded | -| `PrismRequestTooLargeException` | 413 | The request exceeds the provider's size limits | +| `RateLimitedException` | 429 | The provider rate limit was hit, including retry timing when the provider supplies it | +| `ProviderOverloadedException` | 503 | The provider is temporarily overloaded | +| `InsufficientCreditsException` | 402 | The provider account needs topping up; the provider-tagged message is passed through | +| A request that exceeds the provider's size limit | 413 | Detected from the HTTP status, since `laravel/ai` raises a plain `RequestException` here | | Unrecognised exception | 500 | The raw upstream message, or a generic fallback when none is available | -Before returning, the resolver also cleans the message — it extracts the structured `error.message` field from JSON error bodies, collapses whitespace, strips Prism's empty `Details: []` suffix, and truncates to 500 characters. +Before returning, the resolver also cleans the message — it extracts the structured `error.message` field from JSON error bodies, collapses whitespace, strips the empty `Details: []` suffix, and truncates to 500 characters. The `is_known` flag lets `AgentRunner` log appropriately: recognised provider errors are logged as warnings, while unexpected exceptions are logged as errors. Either way, only the friendly `message` is streamed to the client via the SSE `error` event. diff --git a/src/3.0/agentic/building-agent-tools.md b/src/3.0/agentic/building-agent-tools.md index a735fbc7..42bf7d4e 100644 --- a/src/3.0/agentic/building-agent-tools.md +++ b/src/3.0/agentic/building-agent-tools.md @@ -1,6 +1,6 @@ # Building Custom Agent Tools -The AI Agent ships with 38+ PIM tools, but its real power for developers is **extensibility**: any Concord package can register its own tools so the agent can drive *your* catalog logic in natural language. A tool is a small PHP class that the LLM autonomously decides to call based on the user's request. +The AI Agent ships with 35 PIM tools, but its real power for developers is **extensibility**: any Concord package can register its own tools so the agent can drive *your* catalog logic in natural language. A tool is a small PHP class that the LLM autonomously decides to call based on the user's request. This guide walks through building, securing, registering, and testing a custom PIM tool. For the agent's overall architecture, see [AI Agent Integration](./ai-agent.html). @@ -13,24 +13,24 @@ Every tool implements a single-method contract: ```php namespace Webkul\AiAgent\Chat\Contracts; -use Prism\Prism\Tool; +use Laravel\Ai\Contracts\Tool; use Webkul\AiAgent\Chat\ChatContext; interface PimTool { /** - * Return a configured Prism Tool instance. + * Return a configured laravel/ai Tool instance. */ public function register(ChatContext $context): Tool; } ``` -The `register()` method returns a [Prism](https://github.com/prism-php/prism) `Tool` describing: +The `register()` method returns a [`laravel/ai`](https://github.com/laravel/ai) `Tool`. UnoPim provides an abstract base, `Webkul\AiAgent\Chat\Tools\ContextualTool`, that holds the `ChatContext` for you, so a tool implements four methods: -- **`as()`** — the tool name the LLM calls. -- **`for()`** — a description the LLM reads to decide *when* to call it. Be specific; this is the single most important field for correct tool selection. -- **`withStringParameter()` / `withNumberParameter()` / …** — the parameters the LLM must supply. -- **`using()`** — the callback that runs your PIM logic and returns a JSON string. +- **`name()`** — the tool name the LLM calls. +- **`description()`** — what the LLM reads to decide *when* to call it. Be specific; this is the single most important piece of text for correct tool selection. +- **`schema(JsonSchema $schema)`** — the parameters the LLM must supply, returned as an array of schema definitions. +- **`handle(Request $request)`** — the code that runs your PIM logic and returns a JSON string. --- @@ -41,50 +41,89 @@ Say you want the agent to report which products are missing a required attribute ```php namespace App\AiAgent\Tools; -use Prism\Prism\Tool; +use Illuminate\Contracts\JsonSchema\JsonSchema; +use Laravel\Ai\Contracts\Tool; +use Laravel\Ai\Tools\Request; use Webkul\AiAgent\Chat\ChatContext; use Webkul\AiAgent\Chat\Concerns\ChecksPermission; use Webkul\AiAgent\Chat\Contracts\PimTool; +use Webkul\AiAgent\Chat\Tools\ContextualTool; use Webkul\Product\Repositories\ProductRepository; class FindProductsMissingAttribute implements PimTool { - use ChecksPermission; - public function __construct(protected ProductRepository $productRepository) {} public function register(ChatContext $context): Tool { - return (new Tool) - ->as('find_products_missing_attribute') - ->for('Find products that do not have a value for a given attribute code in the current channel and locale. Use when the user asks which products are missing a specific field, e.g. "which electronics are missing voltage".') - ->withStringParameter('attribute_code', 'The attribute code to check, e.g. "voltage" or "description".') - ->withNumberParameter('limit', 'Maximum products to return (default 25).') - ->using(function (string $attribute_code, int $limit = 25) use ($context): string { - // Read-only catalog access still requires the catalog.products permission. - if ($denied = $this->denyUnlessAllowed($context, 'catalog.products')) { + $outer = $this; + + return new class($context, $outer) extends ContextualTool + { + use ChecksPermission; + + public function __construct(ChatContext $context, protected FindProductsMissingAttribute $outer) + { + parent::__construct($context); + } + + public function name(): string + { + return 'find_products_missing_attribute'; + } + + public function description(): string + { + return 'Find products that do not have a value for a given attribute code in the current channel and locale. Use when the user asks which products are missing a specific field, e.g. "which electronics are missing voltage".'; + } + + public function schema(JsonSchema $schema): array + { + return [ + 'attribute_code' => $schema->string()->description('The attribute code to check, e.g. "voltage" or "description".'), + 'limit' => $schema->integer()->description('Maximum products to return (default 25).'), + ]; + } + + public function handle(Request $request): string + { + if ($denied = $this->denyUnlessAllowed($this->context, 'catalog.products')) { return $denied; } - $missing = $this->productRepository->findMissingAttribute( - code: $attribute_code, - channel: $context->channel, - locale: $context->locale, - limit: $limit, + $attributeCode = $request->string('attribute_code')->toString(); + $limit = $request->integer('limit') ?: 25; + + $missing = $this->outer->findMissing( + $attributeCode, + $this->context->channel, + $this->context->locale, + $limit, ); return json_encode([ 'result' => [ - 'attribute' => $attribute_code, + 'attribute' => $attributeCode, 'count' => count($missing), 'products' => $missing, ], ]); - }); + } + }; + } + + /** + * The catalog lookup, kept on the outer class so the anonymous tool stays thin. + */ + public function findMissing(string $code, string $channel, string $locale, int $limit): array + { + return $this->productRepository->findMissingAttribute($code, $channel, $locale, $limit); } } ``` +The anonymous-class pattern is how UnoPim's own tools are written — `register()` returns a `ContextualTool` that already holds the context, while the outer class keeps the injected repositories and any heavy logic. Look at `Webkul\AiAgent\Chat\Tools\ExportProducts` for a complete example. + Now the user can ask *"Which products in Electronics are missing the voltage attribute?"* and the LLM will call this tool with `attribute_code="voltage"`. ::: tip @@ -95,25 +134,25 @@ Always go through a `*Repository` for catalog access rather than querying Eloque ## The `ChatContext` DTO -Every tool callback receives the immutable `ChatContext` carrying request-scoped data — the active channel and locale, the product currently being edited (if the chat was opened from a product page), the AI platform/model, and the authenticated admin for ACL checks: +Every tool receives the immutable `ChatContext` carrying request-scoped data — the active channel and locale, the product currently being edited (if the chat was opened from a product page), the AI platform and model, and the authenticated admin for ACL checks. `ContextualTool` exposes it as `$this->context`: ```php -final class ChatContext +final readonly class ChatContext { public function __construct( - public readonly string $message, // User's text message - public readonly array $history, // Conversation history - public readonly ?int $productId, // Product being edited (page context) - public readonly ?string $productSku, - public readonly ?string $productName, - public readonly string $locale, // Active locale (e.g. en_US) - public readonly string $channel, // Active channel (e.g. default) - public readonly MagicAIPlatform $platform, // AI platform record - public readonly string $model = '', - public readonly array $uploadedImagePaths = [], - public readonly array $uploadedFilePaths = [], - public readonly ?string $currentPage = null, - public readonly ?Admin $user = null, // Authenticated admin (for ACL) + public string $message, // User's text message + public array $history, // Conversation history + public ?int $productId, // Product being edited (page context) + public ?string $productSku, + public ?string $productName, + public string $locale, // Active locale (e.g. en_US) + public string $channel, // Active channel (e.g. default) + public MagicAIPlatform $platform, // AI platform record + public string $model = '', + public array $uploadedImagePaths = [], + public array $uploadedFilePaths = [], + public ?string $currentPage = null, + public ?Admin $user = null, // Authenticated admin (for ACL) ) {} } ``` @@ -129,8 +168,8 @@ Tools that read or write the catalog must respect UnoPim's role-based permission ```php use Webkul\AiAgent\Chat\Concerns\ChecksPermission; -// Inside ->using(): -if ($denied = $this->denyUnlessAllowed($context, 'catalog.products.edit')) { +// Inside handle(): +if ($denied = $this->denyUnlessAllowed($this->context, 'catalog.products.edit')) { return $denied; // returns a JSON error the LLM relays to the user } ``` @@ -146,9 +185,9 @@ Write tools should honour the configured `approval_mode` (`auto`, `review`, `sug ```php use Webkul\AiAgent\Chat\Concerns\QueuesForApproval; -// Inside ->using() for a write tool: +// Inside handle() for a write tool: if ($this->shouldQueueForApproval()) { - return $this->queueChange($context, 'Update voltage on 12 products', [ + return $this->queueChange($this->context, 'Update voltage on 12 products', [ 'type' => 'bulk_edit', 'data' => $changes, 'affected_count' => 12, @@ -191,7 +230,7 @@ The `class_exists` guard keeps your package safe to install even when the AiAgen ## Testing Your Tool -Per the UnoPim development pipeline, every tool needs a Pest test. Test the tool's behaviour through its callback — assert on the JSON contract the LLM will consume: +Per the UnoPim development pipeline, every tool needs a Pest test. Test the tool's behaviour through its `handle()` method — assert on the JSON contract the LLM will consume: ```php it('lists products missing the requested attribute', function () { @@ -200,7 +239,9 @@ it('lists products missing the requested attribute', function () { $tool = app(\App\AiAgent\Tools\FindProductsMissingAttribute::class) ->register($context); - $result = json_decode($tool->handle(attribute_code: 'voltage', limit: 5), true); + $request = new \Laravel\Ai\Tools\Request(['attribute_code' => 'voltage', 'limit' => 5]); + + $result = json_decode($tool->handle($request), true); expect($result['result']['attribute'])->toBe('voltage') ->and($result['result']['count'])->toBeGreaterThanOrEqual(0); @@ -213,9 +254,9 @@ Also assert that a user without the `catalog.products` permission receives the d ## Best Practices -- **Return JSON strings** — every callback returns a JSON-encoded `string`; the LLM parses it to compose its reply. +- **Return JSON strings** — `handle()` returns a JSON-encoded `string`; the LLM parses it to compose its reply. - **Check permissions first** — use `ChecksPermission` on any tool touching the catalog. - **Support approval mode** — use `QueuesForApproval` on write tools. - **Keep tools focused** — one tool, one job. The LLM chains tools for complex workflows. -- **Write a precise `->for()`** — the description drives whether the LLM picks your tool at the right moment. +- **Write a precise `description()`** — it drives whether the LLM picks your tool at the right moment. - **Stay PIM-scoped** — operate on products, attributes, categories, families, channels, and locales through their repositories; honour the active channel/locale from `ChatContext`. diff --git a/src/3.0/agentic/building-integrations.md b/src/3.0/agentic/building-integrations.md index 508b3ed0..6de0341d 100644 --- a/src/3.0/agentic/building-integrations.md +++ b/src/3.0/agentic/building-integrations.md @@ -1,10 +1,10 @@ # Building an Integration with AI -This is the end-to-end walkthrough for developing a real UnoPim integration — a third-party connector — using AI assistance at every step. It ties together the [Agentic Skills](./agent-skills.html) (which teach your coding agent UnoPim's conventions) and the [MCP Server](./mcp-server.html) (which gives it hands on the live instance), and maps each step to the underlying manual documentation so you always know what the AI is generating. +This is the end-to-end walkthrough for developing a real UnoPim integration — a third-party connector — using AI assistance at every step. It builds on the [Agentic Skills](./agent-skills.html), which teach your coding agent UnoPim's conventions, and maps each step to the underlying manual documentation so you always know what the AI is generating. ::: tip Prerequisites - The [`unopim-plugin-development`](./agent-skills.html#unopim-plugin-development) skill installed in your coding agent. -- The [MCP Server](./mcp-server.html) connected to your local instance. +- A local UnoPim checkout your agent can read and write, with a terminal it can run Artisan and Composer in. - Familiarity with [Plugin Development](../plugins/create-plugin.html) — the AI generates this structure; you should be able to read it. ::: @@ -14,18 +14,18 @@ We'll build a connector named **`Acme`** that authenticates against a REST API, ## How the Pieces Work Together -| Step | Skill section | MCP action | Manual reference | -|------|---------------|------------|------------------| -| Scaffold the package | `@core` | `generate_plugin` | [Getting Started](../plugins/create-plugin.html) | -| Models & repositories | `@backend` | `dev_tools` | [Models](../packages/create-models.html), [Repositories](../packages/store-data-through-repositories.html) | -| Credential storage | `@credentials` | `dev_tools` | [Migrations](../packages/create-migrations.html) | -| HTTP client | `@http` | `dev_tools` | — | -| Credentials DataGrid | `unopim-datagrid` skill | `dev_tools` | [DataGrid](../packages/datagrid.html) | -| Attribute mapping | `@mapping` | `dev_tools` | [History Tracking](../packages/history.html) | -| Export profile | `unopim-data-transfer` skill | `dev_tools` | [Export Profile](../plugins/create-export-profile.html) | -| Test & review | `unopim-dev-cycle`, `unopim-code-review` | `generate_test`, `run_command` | — | +| Step | Skill section | Manual reference | +|------|---------------|------------------| +| Scaffold the package | `@core` | [Getting Started](../plugins/create-plugin.html) | +| Models & repositories | `@backend` | [Models](../packages/create-models.html), [Repositories](../packages/store-data-through-repositories.html) | +| Credential storage | `@credentials` | [Migrations](../packages/create-migrations.html) | +| HTTP client | `@http` | — | +| Credentials DataGrid | `unopim-datagrid` skill | [DataGrid](../packages/datagrid.html) | +| Attribute mapping | `@mapping` | [History Tracking](../packages/history.html) | +| Export profile | `unopim-data-transfer` skill | [Export Profile](../plugins/create-export-profile.html) | +| Test & review | `unopim-dev-cycle`, `unopim-code-review` | — | -The skill keeps the AI on UnoPim's rails (Concord package layout, `wk_` table prefix, `Database/Migration/` folder, `['admin']` middleware, repository pattern, cURL over Guzzle, translation keys for all strings). The MCP Server executes the scaffolding and commands against your instance. +The skill keeps the AI on UnoPim's rails (Concord package layout, prefix-safe table names, `src/Database/Migrations/` folder, `['admin']` middleware, repository pattern, cURL over Guzzle, translation keys for all strings). --- @@ -35,7 +35,7 @@ Start from the day-1 checklist the `@quickstart` reference encodes. Prompt your > *"Scaffold an UnoPim connector plugin named 'Acme' with a service provider, config, ACL entry, and an admin menu item under a 'Connectors' group."* -Behind the scenes the agent uses the MCP `generate_plugin` action (`--type=connector`), producing `packages/Webkul/Acme` wired to UnoPim's conventions: +The agent creates `packages/Webkul/Acme` wired to UnoPim's conventions: ``` packages/Webkul/Acme/ @@ -59,11 +59,11 @@ packages/Webkul/Acme/ └── admin-routes.php # middleware: ['admin'] only ``` -Notice what the skill enforced without being asked: the `Database/Migration/` folder is singular, the route file uses `['admin']` middleware only, the ACL config is a flat array, and every label lives in `Resources/lang/`. A generic assistant would get several of these wrong. +Notice what the skill enforced without being asked: migrations land in `src/Database/Migrations/`, the route file uses `['admin']` middleware only, the ACL config is a flat array, and every label lives in `Resources/lang/`. A generic assistant would get several of these wrong. **Verify:** -> *"Run `composer dump-autoload` and confirm the Acme menu item renders in the admin panel."* → MCP `run_command`. +> *"Run `composer dump-autoload` and confirm the Acme menu item renders in the admin panel."* --- @@ -71,19 +71,19 @@ Notice what the skill enforced without being asked: the `Database/Migration/` fo Connectors need a place to store API credentials and a way to test them. The `@credentials` reference covers the model, migration, repository, and connection-test pattern. -> *"Add credential storage for Acme: a `wk_acme_credentials` migration, model, repository, and a CRUD controller. Add a 'Test Connection' action that calls the Acme API and reports success or failure."* +> *"Add credential storage for Acme: an `acme_credentials` migration, model, repository, and a CRUD controller. Add a 'Test Connection' action that calls the Acme API and reports success or failure."* The skill ensures: -- The migration lands in `packages/Webkul/Acme/src/Database/Migration/` (singular folder). +- The migration lands in `packages/Webkul/Acme/src/Database/Migrations/`. - Sensitive fields go in `$auditExclude` — **not** `Crypt::encryptString()` — per UnoPim convention. - All DB access goes through an `AcmeCredentialRepository`. - Controller store/update/delete return `JsonResponse` with `redirect_url` and `message`. -For example, the generated migration follows the `wk_` prefix and singular-folder rules automatically: +For example, the generated migration declares the table unprefixed, so an installation that sets `DB_PREFIX` still works: ```php -// packages/Webkul/Acme/src/Database/Migration/2025_01_01_000000_create_acme_credentials_table.php +// packages/Webkul/Acme/src/Database/Migrations/2026_01_01_000000_create_acme_credentials_table.php Schema::create('acme_credentials', function (Blueprint $table) { $table->id(); $table->string('name'); @@ -94,7 +94,7 @@ Schema::create('acme_credentials', function (Blueprint $table) { }); ``` -> `DB::table('acme_credentials')` — Laravel prepends the `wk_` prefix automatically, so you reference the unprefixed name in code. +> `DB::table('acme_credentials')` — the query builder applies `DB_PREFIX` for you, so you always reference the unprefixed name in code. **Verify:** Save a credential set in the admin panel and run the Test Connection action. @@ -116,7 +116,7 @@ List the stored credentials with search, edit, and delete. Switch to the [`unopi > *"Create a `CredentialDataGrid` for Acme with columns for name and status, an edit action, and a mass-delete action, guarded by the `acme.credentials` permission."* -The skill produces `prepareQueryBuilder` with `DB::table` (unprefixed — Laravel adds `wk_`), `addColumn` closures, `addAction`, `addMassAction`, and a `bouncer()` permission check — matching the [DataGrid](../packages/datagrid.html) engine. +The skill produces `prepareQueryBuilder` with `DB::table` (unprefixed — the builder applies any `DB_PREFIX`), `addColumn` closures, `addAction`, `addMassAction`, and a `bouncer()` permission check — matching the [DataGrid](../packages/datagrid.html) engine. --- @@ -138,7 +138,7 @@ Push products to the remote API through UnoPim's data-transfer pipeline. Switch This mirrors [Export Profile](../plugins/create-export-profile.html) — `exporters.php` registration, an `Exporter` class, a `Validator`, and a queued job with state tracking. -**Verify:** Run the export profile from the admin panel and watch the job's status, counts, and errors. From your AI editor you can also inspect it via the MCP `search_jobs` and `get_job_execution` tools. +**Verify:** Run the export profile from the admin panel and watch the job's status, counts, and errors in the [job tracker](../packages/data-transfer.html). --- @@ -163,6 +163,6 @@ php artisan unopim:translations:check # all locale keys present ## Why AI + Skills Beats AI Alone -A generic AI assistant writes *plausible Laravel* — wrong table prefix, `web` middleware, inline validation, Guzzle, English-only strings. The `unopim-plugin-development` skill injects UnoPim's actual conventions into the agent's context, so the generated connector is **UnoPim-correct on the first pass**. The MCP Server then lets the agent scaffold, migrate, test, and verify against your live instance without you switching tools. +A generic AI assistant writes *plausible Laravel* — wrong table prefix, `web` middleware, inline validation, Guzzle, English-only strings. The `unopim-plugin-development` skill injects UnoPim's actual conventions into the agent's context, so the generated connector is **UnoPim-correct on the first pass** — and the `unopim-dev-cycle` and `unopim-code-review` skills keep it that way through testing and review. For deeper detail on any generated artifact, follow the manual references linked in each step — the AI accelerates the work, but the [Technical Codebase](../packages/create-package.html) and [Plugin Development](../plugins/create-plugin.html) sections remain the source of truth. diff --git a/src/3.0/agentic/extending-mcp.md b/src/3.0/agentic/extending-mcp.md deleted file mode 100644 index 02d95440..00000000 --- a/src/3.0/agentic/extending-mcp.md +++ /dev/null @@ -1,160 +0,0 @@ -# Extending the MCP Bridge - -The [MCP Server](./mcp-server.html) is designed to be extended. When the built-in catalog, settings, and developer tools don't cover a workflow specific to your PIM, you can add new capabilities in two ways: - -| Approach | Effort | Best for | -|----------|--------|----------| -| **Dynamic Skills** | Low-code (Markdown) | Repeatable instruction sequences, no new PHP logic | -| **Core Tools** | PHP | Deep integration, custom catalog logic, transactions | - ---- - -## 1. Dynamic Skills (Low-Code) - -Skills are the fastest way to extend the bridge. A skill is a Markdown file with instructions and parameters — the bridge auto-discovers it and registers it as an MCP tool. - -### Create a Skill - -1. Create a directory under `.ai/skills/` (e.g. `.ai/skills/enrich-missing-descriptions/`). -2. Add a `SKILL.md` with YAML frontmatter and an instruction body: - -```markdown ---- -name: Enrich Missing Descriptions -description: Find catalog products without a description and draft SEO-friendly copy -parameters: - category: - type: string - description: Category code to scope the enrichment - required: true ---- - -# Instructions - -1. Use `search_products` to find products in the given category with no description. -2. For each product, draft a 200-word SEO-friendly description from its name and attributes. -3. Use `upsert_products` to write the descriptions back (batches of 50). -4. Report how many products were enriched. -``` - -The bridge registers this as `execute_enrich_missing_descriptions`. The skill body simply *orchestrates the existing catalog tools* — no PHP required. - -::: tip -This is the same `SKILL.md` format used by the [Agentic Skills](./agent-skills.html). A skill you author for your coding agent can double as an executable MCP tool. -::: - ---- - -## 2. Core Tools (PHP) - -When you need custom catalog logic, transactions, or integration that Markdown can't express, implement a Core Tool. All tools extend `Webkul\MCP\Tools\BaseMcpTool`, which wraps `execute()` with standardized error handling and logging — you implement `execute()` and `schema()`. - -```php -namespace Webkul\MCP\Tools\Catalog; - -use Illuminate\Contracts\JsonSchema\JsonSchema; -use Laravel\Mcp\Request; -use Laravel\Mcp\Response; -use Webkul\MCP\Tools\BaseMcpTool; - -class ProductCompletenessReportTool extends BaseMcpTool -{ - /** - * The tool's name (exposed to the AI client). - */ - public string $name = 'product_completeness_report'; - - /** - * The tool's description. - */ - protected string $description = 'Report completeness percentage for a product SKU in a given channel.'; - - /** - * Throw exceptions for errors — BaseMcpTool logs them with a reference - * ID and returns a safe error response to the client. - */ - protected function execute(Request $request): Response - { - $validated = $request->validate([ - 'sku' => ['required', 'string', 'max:255'], - 'channel' => ['required', 'string', 'max:64'], - ]); - - // Your PIM logic — resolve the product via its repository and - // compute completeness for the requested channel. - $report = [ - 'sku' => $validated['sku'], - 'channel' => $validated['channel'], - 'percentage' => 100, - 'missing' => [], - ]; - - return Response::json($report); - } - - /** - * JSON schema advertised to the AI client. - */ - public function schema(JsonSchema $schema): array - { - return [ - 'sku' => $schema->string() - ->description('Product SKU to evaluate.') - ->required(), - 'channel' => $schema->string() - ->description('Channel code, e.g. "default".') - ->required(), - ]; - } -} -``` - -::: warning Transactions -If your tool performs multiple writes, wrap them in `DB::transaction(...)`, or ensure every early `return Response::error(...)` is preceded by `DB::rollBack()`. Returning early inside an open transaction without a rollback leaks the transaction. -::: - -### Register the Tool - -Add the class to `Webkul\MCP\Registry\ToolRegistry`: - -```php -// packages/Webkul/MCP/src/Registry/ToolRegistry.php - -public static function tools(): array -{ - return [ - // ... existing tools - \Webkul\MCP\Tools\Catalog\ProductCompletenessReportTool::class, - ]; -} -``` - ---- - -## Developing Securely - -The bridge is production-hardened — your tools must uphold the same guarantees: - -- **File operations** — use `FileManagerInterface`, which enforces path-traversal protection within the configured `allowed_paths`. -- **CLI operations** — use `CommandRunner`, which enforces command whitelisting (only `php artisan` and `composer` base commands; shell operators blocked). -- **Validate everything** — always validate arguments in `execute()` before acting. -- **Map to ACL** — write tools should map to the relevant UnoPim permission via `bouncer()` so the same role rules apply over MCP as in the admin panel. - ---- - -## Testing - -Every new tool needs a test in `packages/Webkul/MCP/tests/`: - -- **Unit tests** — for isolated logic (validation, schema, business rules). -- **Feature tests** — for end-to-end execution through the MCP server, including the security layers (auth, rate limit, ACL, audit logging). - -```bash -# From the package -cd packages/Webkul/MCP && ../../vendor/bin/pest - -# From the project root -./vendor/bin/pest --filter=MCP -``` - -Run Laravel Pint after any change and verify with `vendor/bin/pint --test` before opening a PR. diff --git a/src/3.0/agentic/index.md b/src/3.0/agentic/index.md index 870578d0..4745db3c 100644 --- a/src/3.0/agentic/index.md +++ b/src/3.0/agentic/index.md @@ -1,14 +1,13 @@ # Agentic Development -UnoPim is built to be worked on *with* AI — not just to ship an in-app assistant. The **Agentic Development** toolkit gives both end users and developers a set of AI-native interfaces into a UnoPim instance, each tuned for a different audience and workflow. +UnoPim is built to be worked on *with* AI — not just to ship an in-app assistant. The **Agentic Development** toolkit gives both end users and developers AI-native interfaces into a UnoPim instance, each tuned for a different audience and workflow. -There are three pillars: +There are two pillars: | Pillar | Audience | What it does | |--------|----------|--------------| -| **AI Agent** | Catalog managers, business users | A conversational chat widget inside the admin panel with 38+ PIM tools — search, create, bulk edit, enrich, translate, plan multi-step workflows. | +| **AI Agent** | Catalog managers, business users | A conversational chat widget inside the admin panel with 35 PIM tools — search, create, bulk edit, enrich, translate, plan multi-step workflows. | | **Agentic Skills** | Developers using coding agents | Domain-specific skill packs (`SKILL.md`) that teach Claude Code, Cursor, Windsurf, and Copilot how UnoPim's architecture and conventions work, so generated code is correct on the first try. | -| **MCP Server** | Developers & remote AI assistants | A [Model Context Protocol](https://modelcontextprotocol.io/) bridge that exposes the catalog, settings, and developer tools to any MCP-compatible client over HTTP (SSE) or stdio. | --- @@ -16,17 +15,14 @@ There are three pillars: ``` ┌─────────────────────────────────────────────────────────┐ -│ UnoPim Instance │ -│ │ -│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ AI Agent │ │ MCP Server │ │ Catalog & │ │ -│ │ (chat widget)│──▶│ (HTTP + stdio)│──▶│ Settings │ │ -│ └─────────────┘ └──────┬───────┘ └──────────────┘ │ -│ │ │ -└────────────────────────────┼──────────────────────────────┘ - │ MCP protocol - ┌──────────────┼──────────────┐ - ▼ ▼ ▼ +│ UnoPim Instance │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ │ +│ │ AI Agent │───────────▶│ Catalog & │ │ +│ │ (chat widget)│ │ Settings │ │ +│ └──────────────┘ └──────────────┘ │ +└──────────────────────────────────────────────────────────┘ + Claude Code Cursor Copilot ▲ │ Agentic Skills (SKILL.md) @@ -35,8 +31,7 @@ There are three pillars: ``` - The **AI Agent** lives inside the app and talks to your catalog directly. It is the right entry point for non-technical users who want to manage products in natural language. -- The **MCP Server** exposes that same catalog (plus developer tooling) to *external* AI editors, so a developer in Cursor or Claude Code can read, write, and scaffold without leaving the IDE. -- **Agentic Skills** ride alongside the coding agent. They do not call UnoPim themselves — they inject UnoPim's conventions (Concord packages, `wk_` table prefix, repository pattern, cURL connectors, translation rules) into the agent's context so the code it writes — or the code it writes *through* the MCP Server — follows UnoPim standards. +- **Agentic Skills** ride alongside your coding agent. They do not call UnoPim themselves — they inject UnoPim's conventions (Concord packages, prefix-safe table names, repository pattern, cURL connectors, translation rules) into the agent's context so the code it writes follows UnoPim standards. --- @@ -44,10 +39,10 @@ There are three pillars: UnoPim is designed so AI assistants are first-class citizens, not bolted-on afterthoughts: -- **Native tool calling** — the AI Agent ships 38+ PIM tools built on [prism-php/prism](https://github.com/prism-php/prism), so the LLM acts on your catalog directly instead of guessing. -- **A standard protocol** — the MCP Server speaks the open [Model Context Protocol](https://modelcontextprotocol.io/), so any compatible editor connects without custom glue. -- **Codified conventions** — the Agentic Skills encode UnoPim's Concord architecture and coding standards, so generated code follows the rules (repository pattern, `wk_` table prefix, cURL connectors, translation requirements) the first time. -- **Security by default** — ACL enforcement, rate limiting, audit logging, command whitelisting, and approval queues apply to every AI-driven action. +- **Native tool calling** — the AI Agent ships 35 PIM tools built on [`laravel/ai`](https://github.com/laravel/ai), so the LLM acts on your catalog directly instead of guessing. +- **Codified conventions** — the Agentic Skills encode UnoPim's Concord architecture and coding standards, so generated code follows the rules (repository pattern, prefix-safe table names, cURL connectors, translation requirements) the first time. +- **A documented REST API** — anything an assistant cannot do in-app it can do over [the REST API](../api/), which covers the full catalog, media, settings, and passports. +- **Security by default** — ACL enforcement, rate limiting, audit logging, and approval queues apply to every AI-driven action. ## What You Can Build @@ -63,13 +58,11 @@ A few examples of what these tools unlock: "Scaffold a WooCommerce connector plugin with credential storage, a connection test, and an export profile." → Agentic Skills (unopim-plugin-development) - + MCP Server (generate_plugin, run_command) ``` ``` -"What's the schema for products? Then create 20 demo products - with realistic attributes and verify they imported correctly." - → MCP Server (get_catalog_schema + upsert_products + search_products) +"Review my changes against UnoPim standards before I open a PR." + → Agentic Skills (unopim-code-review) ``` --- @@ -78,9 +71,9 @@ A few examples of what these tools unlock: - **"I want to manage my catalog by chatting."** → Use the [AI Agent](./ai-agent.html). - **"I'm coding an UnoPim plugin and want my AI assistant to write correct UnoPim code."** → Install the [Agentic Skills](./agent-skills.html). -- **"I want my AI editor to read/write my live catalog and run Artisan commands."** → Set up the [MCP Server](./mcp-server.html). +- **"I want an external system to read and write my catalog."** → Use the [REST API](../api/). -These are complementary. A typical developer setup installs the Agentic Skills **and** connects the MCP Server: the skills tell the agent *how* to write UnoPim code, and the MCP Server gives it the *hands* to inspect the live instance, scaffold plugins, and verify changes. +The first two are complementary: the AI Agent works inside the admin panel for catalog work, while the skills make your coding agent fluent in UnoPim when you are building on the platform. --- @@ -88,9 +81,7 @@ These are complementary. A typical developer setup installs the Agentic Skills * - **[AI Agent Integration](./ai-agent.html)** — The in-app conversational assistant: chat widget, tool system, approval queues, auto-enrichment, semantic search. - **[MagicAI Platform Management](./magic-ai-platform.html)** — Multi-provider AI configuration powering the agent and content generation. -- **[Agentic Skills](./agent-skills.html)** — The six UnoPim skill packs, how to install them, and how to author your own. -- **[MCP Server](./mcp-server.html)** — Installing the MCP bridge, connecting AI editors, available tools, configuration, and security. -- **[Building an Integration with AI](./building-integrations.html)** — End-to-end walkthrough: scaffold a connector with the skills + MCP, from credentials to export profile to review. +- **[Agentic Skills](./agent-skills.html)** — The UnoPim skill packs, how to install them, and how to author your own. +- **[Building an Integration with AI](./building-integrations.html)** — End-to-end walkthrough: scaffold a connector with the skills, from credentials to export profile to review. - **[Building Custom Agent Tools](./building-agent-tools.html)** — Extend the AI Agent with your own `PimTool` classes: ACL, approval, registration, and testing. -- **[Extending the MCP Bridge](./extending-mcp.html)** — Add custom MCP capabilities via dynamic Skills (low-code) or PHP Core Tools. - **[Agentic Recipes](./recipes.html)** — End-to-end playbooks for building, enriching, and maintaining your catalog with AI. diff --git a/src/3.0/agentic/magic-ai-platform.md b/src/3.0/agentic/magic-ai-platform.md index 336ead48..3801b274 100644 --- a/src/3.0/agentic/magic-ai-platform.md +++ b/src/3.0/agentic/magic-ai-platform.md @@ -2,7 +2,7 @@ ## Introduction -UnoPim v2.0.0 introduced a unified multi-platform AI provider architecture. Instead of individual service classes for each provider, a single **LaravelAiAdapter** bridges all supported AI providers through the `laravel/ai` ^0.3.2 SDK and the Prism library. +UnoPim uses a unified multi-platform AI provider architecture. Instead of individual service classes for each provider, a single **LaravelAiAdapter** bridges every supported AI provider through the [`laravel/ai`](https://github.com/laravel/ai) SDK (`^0.9.1`). Credentials are now managed via a dedicated database table (`magic_ai_platforms`) with encrypted API key storage, replacing the previous configuration-file approach. Administrators can add, test, and switch between providers entirely from the admin panel. @@ -38,7 +38,7 @@ Image generation is currently supported by OpenAI, Gemini, and xAI. Attempting t The **Custom** provider lets you connect any OpenAI-compatible AI service — such as Cerebras, Together, Fireworks, or a self-hosted gateway — without writing a new provider class. ::: -When you select **Custom (OpenAI-compatible)** as the provider, the `LaravelAiAdapter` routes requests through Prism's Groq provider implementation. Groq's provider posts to the legacy `/chat/completions` endpoint, which is the de-facto standard that virtually every OpenAI-compatible third-party service implements. This means any service exposing a `/chat/completions` API will work without further code changes. +When you select **Custom (OpenAI-compatible)** as the provider, the `LaravelAiAdapter` routes requests through the SDK's Groq lab. That lab posts to the legacy `/chat/completions` endpoint, which is the de-facto standard that virtually every OpenAI-compatible third-party service implements, so any service exposing a `/chat/completions` API works without further code changes. ### Configuring a Custom Provider @@ -53,7 +53,7 @@ When you select **Custom (OpenAI-compatible)** as the provider, the `LaravelAiAd ### How the Custom Base URL Is Applied -At runtime, when a platform has an `api_url` set, the adapter dynamically overrides the base URL for both the Laravel AI SDK and Prism: +At runtime, when a platform has an `api_url` set, the adapter dynamically overrides the SDK's base URL: ```php config(["ai.providers.{$configKey}.url" => $this->platform->api_url]); @@ -71,7 +71,7 @@ The custom provider is text-only — `supportsImages()` returns `false` for the ### Before (v1.0.x) -In v1.0.x, each AI provider had its own service class: +In v1.0.x, each AI provider had its own service class — these classes no longer exist: ``` Webkul\MagicAI\Services\OpenAI @@ -82,7 +82,7 @@ Webkul\MagicAI\Services\Ollama Provider credentials were stored in Laravel config files, and switching providers required code or `.env` changes. -### After (v2.0.0) +### After All provider logic is consolidated into a single adapter: @@ -93,8 +93,8 @@ Webkul\MagicAI\Services\LaravelAiAdapter This adapter: - Implements the `Webkul\MagicAI\Contracts\LLMModelInterface` contract. -- Uses **Prism** (`echolabsdev/prism`) for text generation with full control over temperature, max tokens, and system prompts. -- Uses **Laravel AI SDK** (`laravel/ai`) `Image::of()` for image generation. +- Uses the **Laravel AI SDK** (`laravel/ai`) for text generation, with full control over temperature, max tokens, and system prompts. +- Uses the same SDK's `Image::of()` for image generation. - Reads credentials from the `magic_ai_platforms` database table at runtime. ### Key Components @@ -171,7 +171,7 @@ Only one platform can be the default at a time. When a new platform is set as de ### Text Generation -The adapter uses Prism directly for text generation, providing: +The adapter calls the SDK directly for text generation, providing: - Configurable **temperature** (0.0 -- 1.0) - Configurable **max tokens** (automatically increased for reasoning models like o1, o3) @@ -216,7 +216,7 @@ $service = new OpenAI(); $response = $service->ask('Generate a product description for...'); ``` -**After (v2.0.0):** +**After:** ```php use Webkul\MagicAI\Models\MagicAIPlatform; @@ -237,7 +237,7 @@ $adapter = new LaravelAiAdapter( $response = $adapter->ask(); ``` -### Generating Images (v2.0.0) +### Generating Images ```php $adapter = new LaravelAiAdapter( @@ -296,7 +296,6 @@ The `Webkul\MagicAI\Enums\AiProvider` backed enum provides utility methods for e | `configKey()` | Laravel config key for the provider | | `defaultUrl()` | Default API base URL | | `supportsImages()` | Whether the provider supports image generation | -| `toPrismProvider()` | Maps to `Prism\Prism\Enums\Provider` | | `toLab()` | Maps to `Laravel\Ai\Enums\Lab` | | `fetchModels()` | Fetches available models from the provider API | | `options()` | Returns an array suitable for dropdown menus | diff --git a/src/3.0/agentic/mcp-server.md b/src/3.0/agentic/mcp-server.md deleted file mode 100644 index 68bb9026..00000000 --- a/src/3.0/agentic/mcp-server.md +++ /dev/null @@ -1,266 +0,0 @@ -# MCP Server - -The **UnoPim MCP Bridge** lets AI assistants (Claude Code, GitHub Copilot, Cursor, Windsurf) interact with your UnoPim catalog, settings, and codebase through the [Model Context Protocol](https://modelcontextprotocol.io/). It turns your IDE's AI assistant into a first-class UnoPim client — it can search and upsert products, discover the catalog schema, scaffold plugins, run safe Artisan/Composer commands, and execute custom skills, all without leaving the editor. - -The bridge exposes two transports in one package. The **HTTP Agent** serves `POST /api/mcp/unopim` over SSE and is best for remote AI assistants and PIM workflows, while the **stdio Agent** starts with `php artisan mcp:start unopim-dev` and is best for coding agents such as Copilot, Cursor, and Claude Code. - ---- - -## Requirements - -Before installing, make sure your environment runs PHP 8.2+, UnoPim 1.0+, and Laravel 11.0+. - ---- - -## Installation - -Install the package via Composer into your UnoPim root, then run the installer: - -```bash -composer require unopim/mcp -php artisan mcp:install -``` - -`mcp:install` runs Passport scaffolding (if installed), publishes `config/mcp.php` via the `mcp-config` tag, and clears caches. - -Ensure `APP_URL` is set in your `.env`. If you plan to use the HTTP (SSE) transport for remote access, generate an API token for your user. - -### The 10-Second Test - -Verify everything works by launching the inspector — a web UI where you can test each tool manually: - -```bash -php artisan mcp:inspector unopim-dev -``` - ---- - -## Connecting AI Editors - -Register the MCP server in your editor's config. Both transports are supported. - -### VS Code / GitHub Copilot — `.vscode/mcp.json` - -```jsonc -{ - "servers": { - "unopim-dev": { - "command": "php", - "args": ["artisan", "mcp:start", "unopim-dev"], - "cwd": "/path/to/your/unopim" - }, - "unopim-http": { - "url": "http://127.0.0.1:8000/api/mcp/unopim", - "type": "http" - } - } -} -``` - -### Claude Code - -```bash -claude mcp add unopim-dev -- php artisan mcp:start unopim-dev -``` - -### Cursor — `Preferences > Models > MCP` - -Add a new MCP server: **Type** `command`, **Command** `php artisan mcp:start unopim-dev`. - -### Windsurf — `~/.windsurf/mcp.json` - -```jsonc -{ - "servers": { - "unopim-dev": { - "command": "php", - "args": ["artisan", "mcp:start", "unopim-dev"], - "cwd": "/path/to/your/unopim" - } - } -} -``` - ---- - -## Available Tools - -The bridge registers its tools in four groups — catalog, settings, data transfer, and developer tools — so you can quickly find the one that matches the task at hand. - -### Catalog Tools - -Reach for these 13 tools whenever you are reading or writing catalog data — products, categories, attributes, families, and groups. Each entity gets a paired `search_*` and `upsert_*` tool, and `get_catalog_schema` tells your assistant what it can filter on before it starts. - -| Tool | Description | -|------|-------------| -| `get_catalog_schema` | Returns filterable fields, operators, and pagination info per entity. | -| `search_products` | Cursor-paginated product search with filters (max 100 per page). | -| `get_product` | Fetch full product details by ID or SKU with relationships and completeness. | -| `upsert_products` | Batch create/update products (max 50 per call, atomic transaction). | -| `search_categories` | Cursor-paginated category search with filters. | -| `upsert_categories` | Batch create/update categories (max 50 per call, atomic). | -| `search_attributes` | Cursor-paginated attribute search with filters. | -| `upsert_attributes` | Batch create/update attributes (max 50 per call, atomic). | -| `search_attribute_options` | Cursor-paginated search across attribute options. | -| `search_families` | Search attribute families. | -| `upsert_families` | Batch create/update attribute families. | -| `search_attribute_groups` | Search attribute groups. | -| `upsert_attribute_groups` | Batch create/update attribute groups. | - -### Settings Tools - -Use these 4 tools when you need to inspect or change instance-level configuration such as channels, locales, and currencies. - -| Tool | Description | -|------|-------------| -| `search_settings` | Search channels or locales (pass `type`: `channels` or `locales`). | -| `upsert_settings` | Create/update channels or locales (max 50 per call). | -| `search_currencies` | Search currencies with filters. | -| `upsert_currencies` | Batch create/update currencies (max 50 per call). | - -### Data Transfer Tools - -These 2 tools let your assistant monitor the import/export pipeline — useful for checking whether a job ran and what it produced. - -| Tool | Description | -|------|-------------| -| `search_jobs` | Search import/export job instances by `code`, `type`, `entity_type`, `action`. | -| `get_job_execution` | Fetch a single job execution (JobTrack) by ID with status, counts, and errors. | - -### Developer Tools - -When you are building on top of UnoPim rather than managing its data, these 6 core tools (plus dynamically registered skills) handle file operations, safe command execution, and app introspection. - -| Tool | Description | -|------|-------------| -| `dev_tools` | Unified action tool: `create_file`, `read_file`, `update_file`, `run_command`, `generate_plugin`, `generate_test`. | -| `run_skill` | Execute a predefined skill from `.ai/skills/` by name. | -| `get_app_info` | Inspect the host app — Laravel/PHP versions, environment, installed packages. | -| `get_database_schema` | Introspect tables, columns, and relationships. Pass a `table` to scope. | -| `run_database_query` | Execute a read-only SQL query against the configured connection. | -| `read_logs` | Tail entries from `storage/logs/laravel.log` (and named channels). | -| Dynamic Skills | Each `SKILL.md` under `mcp.skills_path` is auto-registered as `execute_`. | - -### Resources and Prompts - -Alongside the tools, the bridge publishes one MCP resource and one prompt: the `catalog-schema` resource gives your assistant a high-level catalog summary (product, category, and attribute counts), and the `analyze-catalog` prompt walks it through a guided catalog analysis covering completeness, consistency, and optimization. - ---- - -## Artisan Commands - -The package also ships a set of Artisan commands for installing, scaffolding, and running the bridge from your terminal. - -| Command | Description | -|---------|-------------| -| `php artisan mcp:install` | Passport scaffolding, publish `config/mcp.php`, clear caches. | -| `php artisan mcp:make plugin [--type=connector\|core-extension\|generic]` | Scaffold a complete UnoPim plugin. | -| `php artisan mcp:make test ` | Generate a Pest test skeleton for a class. | -| `php artisan mcp:dev` | Alias for `mcp:start unopim-dev` — starts the stdio server. | -| `php artisan mcp:start ` | Start an MCP server over stdio. | -| `php artisan mcp:inspector ` | Launch the MCP Inspector against a stdio handle or HTTP path. | - ---- - -## Query Operators - -Every search tool accepts the same filter syntax, so once you learn these operators you can query any entity. - -| Operator | Description | Example | -|----------|-------------|---------| -| `=` | Equals | `{"field": "status", "operator": "=", "value": "active"}` | -| `!=` | Not equals | `{"field": "type", "operator": "!=", "value": "bundle"}` | -| `IN` | In list | `{"field": "type", "operator": "IN", "value": ["simple", "configurable"]}` | -| `NOT IN` | Not in list | `{"field": "id", "operator": "NOT IN", "value": [1, 2]}` | -| `CONTAINS` | Like `%value%` | `{"field": "name", "operator": "CONTAINS", "value": "shirt"}` | -| `STARTS WITH` | Like `value%` | `{"field": "sku", "operator": "STARTS WITH", "value": "PRD"}` | -| `ENDS WITH` | Like `%value` | `{"field": "sku", "operator": "ENDS WITH", "value": "001"}` | -| `>` | Greater than | `{"field": "price", "operator": ">", "value": 100}` | -| `<` | Less than | `{"field": "stock", "operator": "<", "value": 5}` | - ---- - -## Configuration - -`mcp:install` publishes `config/mcp.php`. Key settings: - -```php -return [ - // Require a Bearer token (Passport `auth:api`) for HTTP MCP endpoints. - 'api_auth' => env('MCP_API_AUTH', true), - - // Max requests per minute per tool per caller (IP for HTTP, "cli" for stdio). - 'rate_limit' => env('MCP_RATE_LIMIT', 60), - - // FileManager / dev-tool jail. Anything outside is rejected. - 'allowed_paths' => [ - base_path(), - sys_get_temp_dir(), - ], - - // Log every destructive tool call (upserts, dev_tools mutations). - 'audit_logging' => env('MCP_AUDIT_LOGGING', true), - - // Where SkillLoader looks for SKILL.md files. - 'skills_path' => env('MCP_SKILLS_PATH', base_path('.ai/skills')), - - // Skill registry caching. - 'enable_cache' => env('MCP_ENABLE_CACHE', true), - 'cache_key' => 'mcp.skills', - 'cache_ttl' => env('MCP_CACHE_TTL', 3600), -]; -``` - ---- - -## Dynamic Skills - -Drop a `SKILL.md` with YAML frontmatter into `.ai/skills//SKILL.md` and it is automatically discovered, cached, and registered as an MCP tool named `execute_` — **no code required**. - -```markdown ---- -name: My Custom Skill -description: Automates a specific workflow -license: MIT -parameters: - query: - type: string - required: true -metadata: - author: your-name ---- - -# Instructions - -Describe what this skill does and how to use it. -``` - -This is the same `SKILL.md` format used by the [Agentic Skills](./agent-skills.html) — so a skill authored for your coding agent can double as an executable MCP tool. - ---- - -## Security - -The bridge is production-hardened with multiple layers: - -1. **Request Authentication** — HTTP SSE endpoints default to `auth:api` (OAuth2 via Laravel Passport). -2. **Rate Limiting** — configurable per-minute limit per tool per client (default 60 req/min). -3. **Path Traversal Guard** — all file operations are jailed within configured `allowed_paths` with `.`/`..` normalization. -4. **Command Whitelisting** — only `php artisan` and `composer` base commands are allowed; shell operators (`;`, `&`, `|`, `` ` ``, `$()`, `<`, `>`) are blocked. -5. **ACL Mapping** — every MCP tool maps to internal UnoPim permissions via `bouncer()`. CLI bypasses ACL for local development. -6. **Audit Logging** — all destructive operations (upsert, create, update) are logged with user ID, IP, tool name, and arguments. - ---- - -## Development Workflow - -The MCP bridge collapses the IDE → Terminal → Database → Admin UI loop into a single chat. A typical "AI-first" cycle: - -1. **Discovery** — call `get_catalog_schema` first so the assistant knows your custom attributes and filterable fields. -2. **Scaffolding** — `dev_tools` → `generate_plugin` creates the directory structure, `composer.json`, ServiceProvider, and base classes. -3. **Execution** — `run_command` runs `php artisan migrate`, `db:seed`, etc. securely (base commands only). -4. **Verification** — `read_file` or `search_products` confirms the change landed. - -::: tip -Pair the MCP Server with the [Agentic Skills](./agent-skills.html): the skills teach your agent UnoPim's conventions, and the MCP Server gives it the live instance to act on. -::: diff --git a/src/3.0/agentic/recipes.md b/src/3.0/agentic/recipes.md index 4415c31c..d99a60b5 100644 --- a/src/3.0/agentic/recipes.md +++ b/src/3.0/agentic/recipes.md @@ -2,11 +2,11 @@ Practical, end-to-end playbooks for working on UnoPim *with* AI. Each recipe states the **goal**, which agentic interface it uses, the **steps** (the prompts and tools involved), and what to **verify** afterwards. -The three interfaces these recipes draw on: +The interfaces these recipes draw on: - **AI Agent** — the in-app chat widget ([reference](./ai-agent.html)). -- **MCP Server** — your IDE's AI assistant connected to the live instance ([reference](./mcp-server.html)). - **Agentic Skills** — UnoPim conventions loaded into your coding agent ([reference](./agent-skills.html)). +- **REST API** — for anything driven from outside the admin panel ([reference](../api/)). --- @@ -14,17 +14,17 @@ The three interfaces these recipes draw on: **Goal:** Stand up a new third-party connector plugin without hand-writing boilerplate. -**Uses:** Agentic Skills (`unopim-plugin-development`) + MCP Server (`dev_tools` → `generate_plugin`, `run_command`). +**Uses:** Agentic Skills (`unopim-plugin-development`). **Steps:** 1. With the `unopim-plugin-development` skill installed, prompt your coding agent: > *"Scaffold a connector plugin named 'ShipStation' with a configuration page, credential storage, and a connection test."* -2. The skill supplies UnoPim's package conventions (service provider, `Database/Migration/` folder, `['admin']` middleware, repository pattern); the MCP `generate_plugin` action writes the directory structure, `composer.json`, and base classes into `packages/Webkul/ShipStation`. -3. Run migrations through the bridge: - > *"Run `php artisan migrate`."* → MCP `run_command`. +2. The skill supplies UnoPim's package conventions (service provider, `src/Database/Migrations/` folder, `['admin']` middleware, repository pattern, prefix-safe table names), and the agent writes the directory structure, `composer.json`, and base classes into `packages/Webkul/ShipStation`. +3. Ask it to wire the package up and migrate: + > *"Run `composer dump-autoload`, then `php artisan migrate`."* -**Verify:** Ask the agent to `read_file` the generated `ServiceProvider`, then confirm the plugin's config page loads in the admin panel. Run `vendor/bin/pint --test`. +**Verify:** Read the generated `ServiceProvider`, confirm the plugin's config page loads in the admin panel, and run `vendor/bin/pint --test`. --- @@ -32,35 +32,31 @@ The three interfaces these recipes draw on: **Goal:** Fill missing descriptions across hundreds of products. -**Uses:** AI Agent (search + content generation + bulk edit + auto-translation). +**Uses:** AI Agent (`search_products`, `generate_content`, `bulk_edit`). **Steps:** -1. > *"Find products without a description created in the last 24 hours."* → the agent calls `SearchProducts`. -2. > *"Generate a 200-word SEO-friendly description for each from its name and attributes."* → `GenerateContent`. -3. > *"Apply the descriptions and translate them to all active locales."* → `BulkEdit`, then the `TranslateProductValuesJob` runs on the queue. +1. > *"Find products without a description created in the last 24 hours."* → the agent calls `search_products`. +2. > *"Generate a 200-word SEO-friendly description for each from its name and attributes."* → `generate_content`. +3. > *"Apply the descriptions and translate them to all active locales."* → `bulk_edit`, then translation runs on the queue. **Verify:** Spot-check a few products in the admin panel; confirm locale-dependent fields populated for each active locale. If `approval_mode` is `review`, approve the changeset from the [approval queue](./ai-agent.html#approval-queue). -::: tip -For products created before auto-translation was enabled, run the CLI alternative: `php artisan unopim:translate`. -::: - --- ## Recipe 3 — Catalog Completeness Diagnostics **Goal:** Find why a product isn't appearing in a channel and fix the gaps. -**Uses:** MCP Server (`get_product`, `get_catalog_schema`, `upsert_products`). +**Uses:** AI Agent (`get_product_details`, `data_quality_report`, `update_product`). **Steps:** -1. > *"Check product SKU 'PHONE-001' completeness for channel 'default'. If it's below 100%, list the missing required attributes."* -2. The assistant fetches the product with its completeness data and reports the missing required attributes. -3. > *"Fill the missing attributes with sensible values and upsert."* → `upsert_products` (max 50 per atomic batch). +1. > *"Check product SKU 'PHONE-001' completeness for channel 'default'. If it's below 100%, list the missing required attributes."* → `get_product_details`, which returns the product's completeness data. +2. > *"Show me the same gaps across the whole Electronics category."* → `data_quality_report`. +3. > *"Fill the missing attributes on PHONE-001 with sensible values."* → `update_product`. -**Verify:** Re-run the completeness check; confirm 100% for the target channel. Confirm the product now surfaces in that channel. +**Verify:** Re-run the completeness check; confirm 100% for the target channel and that the product now surfaces in that channel. --- @@ -68,7 +64,7 @@ For products created before auto-translation was enabled, run the CLI alternativ **Goal:** Restructure the catalog — move a set of products to a new category branch. -**Uses:** AI Agent (`SearchProducts`, `AssignCategories`, `BulkEdit`). +**Uses:** AI Agent (`search_products`, `assign_categories`, `bulk_edit`). **Steps:** @@ -83,14 +79,14 @@ For products created before auto-translation was enabled, run the CLI alternativ **Goal:** Rapidly prototype catalog structure during development. -**Uses:** MCP Server (`upsert_attributes`, `upsert_attribute_groups`) or AI Agent (`CreateAttribute`, `ManageFamilies`). +**Uses:** AI Agent (`create_attribute`, `manage_attribute_options`, `manage_families`). **Steps:** 1. > *"Create a select-type attribute 'Fabric Material' with options Cotton, Silk, Wool."* 2. > *"Add it to the 'Clothing' attribute group."* -**Verify:** Confirm the attribute and its options exist via `search_attributes`, and that it renders on a product in the relevant family. +**Verify:** Confirm the attribute and its options exist with `list_attributes`, and that it renders on a product in the relevant family. --- @@ -98,13 +94,13 @@ For products created before auto-translation was enabled, run the CLI alternativ **Goal:** Ship a change that passes the UnoPim development pipeline. -**Uses:** Agentic Skills (`unopim-dev-cycle`, `unopim-code-review`) + MCP Server (`generate_test`, `run_command`). +**Uses:** Agentic Skills (`unopim-dev-cycle`, `unopim-code-review`). **Steps:** -1. > *"Generate a Pest test for the new `ShipStationRepository`."* → MCP `generate_test` (or `mcp:make test`). -2. > *"Run the test."* → `run_command` → `vendor/bin/pest`. -3. > *"Review my changes against UnoPim standards before I open a PR."* → the `unopim-code-review` skill flags convention violations (table prefix, route middleware, hardcoded strings, repository pattern). +1. > *"Generate a Pest test for the new `ShipStationRepository`."* +2. > *"Run the test."* → `vendor/bin/pest`. +3. > *"Review my changes against UnoPim standards before I open a PR."* → the `unopim-code-review` skill flags convention violations (prefix-unsafe SQL, route middleware, hardcoded strings, repository pattern). **Verify:** Tests pass, `vendor/bin/pint --test` reports zero issues, and `php artisan unopim:translations:check` passes with zero missing keys. @@ -114,22 +110,42 @@ For products created before auto-translation was enabled, run the CLI alternativ **Goal:** Stop re-typing a multi-step instruction you run often. -**Uses:** Dynamic Skills (MCP) — see [Extending the MCP Bridge](./extending-mcp.html#dynamic-skills-low-code). +**Uses:** Agentic Skills (your own). + +**Steps:** + +1. Write a `SKILL.md` for `audit-channel-pricing` describing the steps (search products, compare prices across two channels, report mismatches) and install it alongside the UnoPim skills. +2. Your coding agent loads it whenever the task matches its frontmatter. +3. Invoke it by name instead of re-explaining the sequence. + +**Verify:** Run the workflow and confirm it produces the expected mismatch report. + +--- + +## Recipe 8 — Scheduled Delta Sync over the API + +**Goal:** Push only what changed to a downstream system, on a schedule. + +**Uses:** [REST API](../api/) — date filters plus cursor pagination. **Steps:** -1. Drop a `SKILL.md` into `.ai/skills/audit-channel-pricing/` describing the steps (search products, compare prices across two channels, report mismatches). -2. The bridge registers `execute_audit_channel_pricing`. -3. Invoke it any time instead of re-explaining the sequence. +1. Store the timestamp of your last successful run. +2. Request the products changed since then, in cursor mode: + ``` + GET {{url}}/api/v1/rest/products + ?filters={"updated_at":[{"operator":">=","value":"2026-08-01 00:00:00"}]} + &pagination_type=search_after&limit=100 + ``` +3. Follow `links.next` until it comes back `null`, then record the new timestamp. -**Verify:** Run the new tool from your AI editor and confirm it produces the expected mismatch report. +**Verify:** Compare the number of records processed against the admin product grid filtered by the same date range. See [Migrating an API Client](../api/migrating-your-client) for the pagination and rate-limit rules a client must respect. --- ## General Best Practices -- **Discover before acting** — call `get_catalog_schema` first so the assistant knows your custom attributes and filterable fields. -- **Keep batches atomic** — `upsert_*` tools cap at 50 items per call for clean error reporting. +- **Discover before acting** — ask for a `catalog_summary` or `list_attributes` first so the assistant knows your custom attributes and families. - **Use `review` mode for bulk changes** — queue AI-driven mutations as changesets so they're auditable and reversible before they hit the catalog. - **Sequence read → write → verify** — read the current state, make the change, then confirm with a follow-up search or completeness check. - **Let skills enforce conventions** — keep the relevant Agentic Skill active so generated code respects UnoPim's standards the first time. diff --git a/src/3.0/api/association_types.md b/src/3.0/api/association_types.md new file mode 100644 index 00000000..75fd65d3 --- /dev/null +++ b/src/3.0/api/association_types.md @@ -0,0 +1,153 @@ +# Association Types + +Association types define the named product-to-product relationships your catalog supports — the three built-in ones (`related_products`, `up_sells`, `cross_sells`) plus any custom type you add, each optionally carrying per-link fields. This page covers managing them over the REST API. For the concept and the admin UI, see [Configurable Associations](../packages/configurable-associations). + +### Common Headers + +Every request on this page sends the same two headers: + +| Key | Value | +| ------------- | --------------------- | +| Accept | application/json | +| Authorization | Bearer `access_token` | + +## Get All Association Types + +``` +GET {{url}}/api/v1/rest/association-types +``` + +**Headers** — use the [Common Headers](#common-headers). + +The endpoint accepts these query parameters: + +| Name | Info | Type | Default | +|-----------|--------------------------------------------------------------------------|--------|---------| +| `limit` | Records per request. Clamped to a maximum of `100` | Number | `10` | +| `page` | Page number to retrieve | Number | `1` | +| `filters` | Filter by `code` (`=`, `IN`, `NOT IN`) or `status` (`=`) | JSON | N/A | + +### Response + +::: details Response +```json +{ + "data": [ + { + "code": "related_products", + "status": true, + "position": 1, + "is_user_defined": false, + "labels": { + "en_US": "Related Products", + "fr_FR": "Produits associés" + } + }, + { + "code": "spare_parts", + "status": true, + "position": 4, + "is_user_defined": true, + "labels": { + "en_US": "Spare Parts" + } + } + ], + "current_page": 1, + "last_page": 1, + "total": 2 +} +``` +::: + +`is_user_defined` is `false` for the three built-in types. Those are protected: they cannot be deleted, and their code cannot change. + +## Get an Association Type by Code + +``` +GET {{url}}/api/v1/rest/association-types/{code} +``` + +## Create an Association Type + +``` +POST {{url}}/api/v1/rest/association-types +``` + +Send the code plus a name per locale. Locale keys are validated against the active locales: + +```json +{ + "code": "spare_parts", + "status": true, + "en_US": { "name": "Spare Parts" }, + "fr_FR": { "name": "Pièces détachées" } +} +``` + +The `code` must be unique, pass the standard code rule (letters, numbers, underscores, no leading digit), and must not collide with a reserved product field. + +### Response + +```json +{ + "success": true, + "message": "Association type created successfully." +} +``` + +## Update an Association Type + +``` +PUT {{url}}/api/v1/rest/association-types/{code} +PATCH {{url}}/api/v1/rest/association-types/{code} +``` + +`PUT` replaces the submitted attributes; `PATCH` changes only the keys you send. + +## Delete an Association Type + +``` +DELETE {{url}}/api/v1/rest/association-types/{code} +``` + +Deleting a type removes its links from every product. The three built-in types are refused with a `422`. + +## Association Type Fields + +Custom types may carry per-link fields — a quantity on a spare part, a note on a related product. Fields are managed under the type: + +``` +GET {{url}}/api/v1/rest/association-types/{code}/fields +POST {{url}}/api/v1/rest/association-types/{code}/fields +PUT {{url}}/api/v1/rest/association-types/{code}/fields/{fieldCode} +DELETE {{url}}/api/v1/rest/association-types/{code}/fields/{fieldCode} +``` + +### Field Response + +::: details Response +```json +{ + "data": [ + { + "code": "quantity", + "type": "text", + "status": true, + "validation": "numeric", + "position": 1, + "is_required": 1, + "is_unique": 0, + "value_per_locale": 0, + "labels": { + "en_US": "Quantity" + } + } + ] +} +``` +::: + +Field codes are validated the same way as type codes, and `code`, `type`, and `locale` are reserved. + +Values for these fields travel in a product's `associations` payload as `additional_data` — see [Product Associations](./product#product-associations). diff --git a/src/3.0/api/attribute.md b/src/3.0/api/attribute.md index ef44ffa4..80a2d939 100644 --- a/src/3.0/api/attribute.md +++ b/src/3.0/api/attribute.md @@ -25,7 +25,7 @@ The endpoint accepts the following query parameters: | Name | Info | Type | Default | |----------|-------------------------------------------------|--------|---------| -| `limit` | Maximum number of records to return per request | Number | `10` | +| `limit` | Records per request. Clamped to a maximum of `100` | Number | `10` | | `page` | Page number to retrieve based on the limit | Number | `1` | | `filters`| Criteria to filter the records returned | JSON | N/A | diff --git a/src/3.0/api/attribute_families.md b/src/3.0/api/attribute_families.md index 4ff8654a..2f6473b5 100644 --- a/src/3.0/api/attribute_families.md +++ b/src/3.0/api/attribute_families.md @@ -21,6 +21,20 @@ GET {{url}}/api/v1/rest/families **Headers:** use the [Common Headers](#common-headers). +The endpoint accepts these query parameters: + +| Name | Info | Type | Default | +|-----------|-------------------------------------------------------------|--------|---------| +| `limit` | Records per request. Clamped to a maximum of `100` | Number | `10` | +| `page` | Page number to retrieve | Number | `1` | +| `filters` | Filter by `code` with the `=`, `IN`, or `NOT IN` operators | JSON | N/A | + +For example: + +```http +GET {{url}}/api/v1/rest/families?limit=50&filters={"code":[{"operator":"IN","value":["accessories","default"]}]} +``` + ### Response The response contains the list of attribute families with pagination metadata: diff --git a/src/3.0/api/attribute_groups.md b/src/3.0/api/attribute_groups.md index 251ae9e8..7e3f5efc 100644 --- a/src/3.0/api/attribute_groups.md +++ b/src/3.0/api/attribute_groups.md @@ -23,9 +23,11 @@ GET {{url}}/api/v1/rest/attribute-groups The endpoint accepts the following query parameter: -| Name | Info | Type | Default | -|--------|------------------------------------|--------|---------| -| `page` | The page number to retrieve | Number | `1` | +| Name | Info | Type | Default | +|-----------|-------------------------------------------------------------|--------|---------| +| `limit` | Records per request. Clamped to a maximum of `100` | Number | `10` | +| `page` | The page number to retrieve | Number | `1` | +| `filters` | Filter by `code` with the `=`, `IN`, or `NOT IN` operators | JSON | N/A | For example, to fetch a specific page of attribute groups: @@ -33,6 +35,12 @@ For example, to fetch a specific page of attribute groups: GET {{url}}/api/v1/rest/attribute-groups?page=1 ``` +To filter by code: + +```http +GET {{url}}/api/v1/rest/attribute-groups?filters={"code":[{"operator":"IN","value":["marketing","technical"]}]} +``` + ### Response The response contains the list of attribute groups with pagination metadata: diff --git a/src/3.0/api/attribute_options.md b/src/3.0/api/attribute_options.md index dc181478..d129d46c 100644 --- a/src/3.0/api/attribute_options.md +++ b/src/3.0/api/attribute_options.md @@ -47,6 +47,22 @@ The response is the list of options for the specified attribute: ``` ::: +Options come back ordered by `sort_order`, and the response is a plain array — this endpoint is not paginated. + +When the attribute's swatch type is `image` or `color`, each option carries two extra keys: + +```json +{ + "code": "red", + "sort_order": 1, + "labels": { "en_US": "Red" }, + "swatch_value": "attribute_option/12/nEr4h2Kq….png", + "swatch_value_url": "https://example.com/storage/attribute_option/12/nEr4h2Kq….png" +} +``` + +Use [Swatch Media Upload](./media#swatch-media-upload) to set an image swatch. + ## Create Attribute Options by Attribute Code Creates one or more options for an attribute in a single request. @@ -143,3 +159,5 @@ A successful deletion returns a confirmation message: "message": "Deleted successfully." } ``` + +An unknown attribute code or option code returns `404`. Deleting an option does not rewrite products that already store its value — clean those up before removing an option that is in use. diff --git a/src/3.0/api/authenticate.md b/src/3.0/api/authenticate.md index 77c3c448..5750f4c5 100644 --- a/src/3.0/api/authenticate.md +++ b/src/3.0/api/authenticate.md @@ -85,3 +85,15 @@ You receive a fresh access token and a new refresh token: "refresh_token": "def50200b3b5e3ec4f608279263600e4041189dccaabe3e16ad487e72c26b80a128fcfe6232d21c4c9441ee258b8f03964f4685643865677e2e2613a7ed7251849f13934028f84dc33cbf589a6ce0c37e98066e561fbe16997751a831dd9df294a690c3ac43beab27cb86e64fa7eb1fe572c514fb5487d929dfc6b415f34803ce7168cd468fbc9a0f30b460244a8b9da559ec5dfe1b7b01f52219150e02d75a001007fed26a1fd66f2086fed15e4961f9481fdbdac032b3c055e5d6509e615e831fa6b395195cc561b14be95d9f16bf73a77bedcf20b9348e11d1a2a8bab3abaa62f585f1aa804e53b7f8e297b295a18b146eea8ada82ee4ea8d4e6cfdd563f1f06947b5b84ad1e02551674d302a77d1f0949f10324e37ed7c55620c0271a871555784b3d256ca7a48d261ca7afcac50235ae75066a73b7dd99034549a0c9cefb98685527f32b05f13cb681432919644766bc56fb1ad3412c43e96037cd1511d175460ee6f0d5e12a7e2ab90b2ae6e12be2e1a1f62f40ffe80457cd96f850adb5e3c4d23" } ``` + +## Rate Limit + +Token issue and refresh are throttled to **10 requests per minute** by default (`OAUTH_TOKEN_RATE_LIMIT`). A client that requests a token before every API call will exhaust that budget immediately. + +Cache the access token for its lifetime — `expires_in` tells you how long, one hour by default — and refresh only when it expires or a call returns `401`. On a `429`, wait for the `Retry-After` header before retrying. + +Token lifetimes are configurable per installation with `ACCESS_TOKEN_TTL` and `REFRESH_TOKEN_TTL`, so read `expires_in` rather than assuming 3600. + +::: warning Tokens issued before v3.0 are invalid +v3.0 replaces the previously shared OAuth signing keys with per-installation keys, so every token issued by an earlier version stops working at upgrade. Authenticate again. See [Migrating an API Client](./migrating-your-client). +::: diff --git a/src/3.0/api/category.md b/src/3.0/api/category.md index e3a311e4..bc14ee14 100644 --- a/src/3.0/api/category.md +++ b/src/3.0/api/category.md @@ -23,10 +23,11 @@ GET {{url}}/api/v1/rest/categories You can shape the result set with these query parameters: -| Name | Info | Type | Default | -|-----------|----------------------------------------------|--------|---------| -| `filters` | Filter by parent category (e.g., `master`) | JSON | N/A | -| `page` | Page number to retrieve | Number | `1` | +| Name | Info | Type | Default | +|-----------|---------------------------------------------------------------------------------|--------|---------| +| `limit` | Records per request. Clamped to a maximum of `100` | Number | `10` | +| `page` | Page number to retrieve | Number | `1` | +| `filters` | Filter by `code` (`=`, `IN`, `NOT IN`) or `parent` (`=`, e.g. `master`) | JSON | N/A | #### Usage Examples diff --git a/src/3.0/api/category_field_options.md b/src/3.0/api/category_field_options.md index 1bb3160f..4f80fdd0 100644 --- a/src/3.0/api/category_field_options.md +++ b/src/3.0/api/category_field_options.md @@ -194,3 +194,5 @@ A successful deletion returns a confirmation message: "message": "Deleted successfully." } ``` + +An unknown category field code or option code returns `404`. Deleting an option does not rewrite products that already store its value — clean those up before removing an option that is in use. diff --git a/src/3.0/api/category_fields.md b/src/3.0/api/category_fields.md index 118f871f..75afb44e 100644 --- a/src/3.0/api/category_fields.md +++ b/src/3.0/api/category_fields.md @@ -23,10 +23,11 @@ GET {{url}}/api/v1/rest/category-fields The endpoint accepts the following query parameters: -| Name | Description | Type | Default | -|---------|-------------------------------|--------|---------| -| `limit` | Number of records to return | Number | `100` | -| `page` | Page number for pagination | Number | `1` | +| Name | Description | Type | Default | +|-----------|----------------------------------------------------------------------|--------|---------| +| `limit` | Number of records to return. Clamped to a maximum of `100` | Number | `10` | +| `page` | Page number for pagination | Number | `1` | +| `filters` | Filter by `code` or `type` with the `=`, `IN`, or `NOT IN` operators | JSON | N/A | For example, to retrieve the first page with up to 100 category fields: diff --git a/src/3.0/api/channel.md b/src/3.0/api/channel.md index 472164fb..0280b8ae 100644 --- a/src/3.0/api/channel.md +++ b/src/3.0/api/channel.md @@ -21,6 +21,13 @@ GET {{url}}/api/v1/rest/channels **Headers:** use the [Common Headers](#common-headers). +The endpoint accepts these query parameters: + +| Name | Info | Type | Default | +|---------|----------------------------------------------------|--------|---------| +| `limit` | Records per request. Clamped to a maximum of `100` | Number | `10` | +| `page` | Page number to retrieve | Number | `1` | + ### Response The response contains the list of channels with pagination metadata: diff --git a/src/3.0/api/configurable_products.md b/src/3.0/api/configurable_products.md index 61225bf8..aef85aa2 100644 --- a/src/3.0/api/configurable_products.md +++ b/src/3.0/api/configurable_products.md @@ -25,7 +25,7 @@ You can shape the result set with these query parameters: | Name | Info | Type | Default | |-----------|-------------------------------------------------|--------|---------| -| `limit` | The number of products to retrieve per request | Number | `10` | +| `limit` | Products per request. Clamped to a maximum of `100` | Number | `10` | | `page` | Page number to retrieve | Number | `1` | | `filters` | Criteria to filter the records returned | JSON | N/A | @@ -139,8 +139,8 @@ The response returns a paginated list of configurable products in JSON format: "last_page": 1, "total": 1, "links": { - "first": "{{url}}/api/v1/rest/configrable-products?%3Flimit=10&page=1", - "last": "{{url}}/api/v1/rest/configrable-products?%3Flimit=10&page=1", + "first": "{{url}}/api/v1/rest/configurable-products?limit=10&page=1", + "last": "{{url}}/api/v1/rest/configurable-products?limit=10&page=1", "next": null, "prev": null } @@ -477,6 +477,29 @@ A successful creation returns a confirmation message: ``` ::: +## Delete a Configurable Product + +Deletes the configurable product identified by its SKU, along with its variants. + +``` +DELETE {{url}}/api/v1/rest/configurable-products/{sku} +``` + +**Headers** — use the [Common Headers](#common-headers). + +### Response + +```json +{ + "success": true, + "message": "Product deleted successfully." +} +``` + +::: warning +Deleting a configurable product removes every variant beneath it. There is no undo. +::: + ## Deprecated Alias The misspelled `configrable-products` prefix still answers with identical behavior but returns RFC 8594 deprecation headers: diff --git a/src/3.0/api/configuration.md b/src/3.0/api/configuration.md index ef2bb55d..3f158341 100644 --- a/src/3.0/api/configuration.md +++ b/src/3.0/api/configuration.md @@ -17,12 +17,14 @@ Follow these steps in the admin panel: - Under the API Keys section, click the **Create** button to start the process of creating a new API key. 3. **General Section**: - - In the **General** Section, provide the following details: - - **Name**: Enter a unique name for the API key. - - **Assign User**: Choose the user who will be assigned to this API key. + - In the **General** Section, enter a unique **Name** for the API key. ![API key General section](/assets/2.1/images/api-integration-general.png) + ::: tip No user to choose + In v2.x you picked an existing administrator to own the integration. Since v3.0, UnoPim provisions a dedicated **robot user** for each integration automatically — a least-privilege `type = 'api'` account that cannot log into the admin panel. There is nothing to assign. + ::: + 4. **Access Control**: - Navigate to the **Access Control** Section, where you'll configure permissions for the API key. @@ -39,6 +41,10 @@ Follow these steps in the admin panel: ![Save API credentials](/assets/2.1/images/api-integration-save.png) + ::: warning Copy the password now + Saving reveals the robot user's **username** and **password** exactly once. The password is hashed immediately and cannot be shown again — copy both before leaving the screen. If you lose them, use **Regenerate Password** on the integration's edit screen; doing so revokes every token issued to that integration. + ::: + 6. **Generate Secret Key**: - Once saved, a **Generate Secret Key** button will appear. - Click on this button to display the **Client ID** and **Secret Key**. @@ -49,10 +55,10 @@ Follow these steps in the admin panel: ![Client ID and Secret Key](/assets/2.1/images/api-client-id-secret.png) ::: tip Re-Generate Secret Key -After generating the secret key, a **Re-Generate Secret Key** button will be available. Use this button to regenerate the secret key if needed. +After generating the secret key, a **Re-Generate Secret Key** button will be available. Use this button to regenerate the secret key if needed. Like regenerating the password, it revokes existing tokens. ::: -For more detailed reference, you can consult the UnoPim **User Guide** [here](https://docs.unopim.com/1.0/configuration/integration.html). +You now hold the four values every client needs: **Client ID**, **Secret Key**, **username**, and **password**. Continue to [Authentication](authenticate) to exchange them for an access token. ## Set up Postman diff --git a/src/3.0/api/currency.md b/src/3.0/api/currency.md index 06418a0b..174e0653 100644 --- a/src/3.0/api/currency.md +++ b/src/3.0/api/currency.md @@ -25,7 +25,7 @@ The endpoint accepts the following query parameters: | Name | Description | Type | Default | |----------|--------------------------------------------------|--------|---------| -| `limit` | Maximum number of records per request | Number | `100` | +| `limit` | Records per request. Clamped to a maximum of `100` | Number | `10` | | `page` | Page number to retrieve based on the limit | Number | `1` | | `filters`| Criteria to filter the records returned | JSON | N/A | diff --git a/src/3.0/api/index.md b/src/3.0/api/index.md index d9295fe0..7aa0aa4a 100644 --- a/src/3.0/api/index.md +++ b/src/3.0/api/index.md @@ -16,6 +16,10 @@ Here is what you get out of the box: - **Pagination Support**: Streamline data handling for larger datasets through pagination. - **PIM Integration**: Facilitate seamless integration with eCommerce platforms, mobile apps, and other systems that rely on product information management. +## Coming from v2.x + +New in 3.0 is covered in [What's New in v3.0](./whats-new-v3). If you maintain a client built against v2.x, [Migrating an API Client to v3.0](./migrating-your-client) covers what changed underneath it — permissions are now enforced on reads as well as writes, error responses share a single shape, rate limits are enforced, and `limit` is capped at 100. + ## Explore the REST API Demo Try out the UnoPim API through our interactive demo. This demo showcases the Create, Read, and Update operations and other API functionalities, providing developers with hands-on experience. diff --git a/src/3.0/api/locales.md b/src/3.0/api/locales.md index 0d41c388..ea42a9d0 100644 --- a/src/3.0/api/locales.md +++ b/src/3.0/api/locales.md @@ -25,7 +25,7 @@ The endpoint accepts the following query parameters: | Name | Description | Type | Default | |----------|--------------------------------------------------|--------|---------| -| `limit` | Maximum number of records per request | Number | `10` | +| `limit` | Records per request. Clamped to a maximum of `100` | Number | `10` | | `page` | Page number to retrieve based on the limit | Number | `1` | | `filters`| Criteria to filter the records returned | JSON | N/A | diff --git a/src/3.0/api/media.md b/src/3.0/api/media.md index b591b066..0ce159cf 100644 --- a/src/3.0/api/media.md +++ b/src/3.0/api/media.md @@ -109,10 +109,52 @@ The stored file path is returned so you can reference it in category data: ``` ::: +## Swatch Media Upload + +Uploads a swatch image for an attribute option. The attribute's swatch type must be **Image**, or the request fails with `422`. + +``` +POST {{url}}/api/v1/rest/media-files/swatch +``` + +**Headers** — use the [Common Headers](#common-headers). + +The request takes these parameters: + +| Name | Description | Type | +|------------------|-----------------------------------------------------------------|--------| +| `file` | The swatch image: `jpeg`, `png`, `jpg`, `webp`, or `svg`, max 2 MB | File | +| `code` | Code of the attribute option the swatch belongs to | String | +| `attribute_code` | Code of the attribute that owns the option | String | + +### Response + +The stored path and its public URL come back on success: + +::: details Response +```json +{ + "success": true, + "message": "Attribute option updated successfully.", + "data": { + "code": "red", + "swatch_value": "attribute_option/12/nEr4h2Kq….png", + "swatch_value_url": "https://example.com/storage/attribute_option/12/nEr4h2Kq….png" + } +} +``` +::: + +::: tip Never build the path yourself +The stored filename is generated, not taken from the uploaded file. Always use the `swatch_value` returned here. +::: + ## Read Media Lists the file paths already stored for a product, category, or swatch. Media files are identified by query parameters, not path segments: +Product media is scoped like any other attribute value. Pass `channel` and `locale` when the attribute is channel- or locale-scoped; omit them and the default channel and its default locale are used. + ``` GET {{url}}/api/v1/rest/media-files/product?sku=shirt-1&attribute=image GET {{url}}/api/v1/rest/media-files/category?code=apparel&category_field=banner @@ -133,12 +175,16 @@ The matching file paths come back as a simple array: ## Delete Media -Removes stored media using the same parameter scheme with the `DELETE` verb: +Removes stored media using the same parameter scheme with the `DELETE` verb. The file is deleted from storage and the value cleared: ``` DELETE {{url}}/api/v1/rest/media-files/product?sku=shirt-1&attribute=image +DELETE {{url}}/api/v1/rest/media-files/category?code=apparel&category_field=banner +DELETE {{url}}/api/v1/rest/media-files/swatch?code=red&attribute_code=color ``` +If no file is stored at that scope, the response is `404`. + ### Response A successful deletion returns a confirmation message: diff --git a/src/3.0/api/migrating-your-client.md b/src/3.0/api/migrating-your-client.md new file mode 100644 index 00000000..f90de56b --- /dev/null +++ b/src/3.0/api/migrating-your-client.md @@ -0,0 +1,250 @@ +# Migrating an API Client to v3.0 + +[What's New in v3.0](./whats-new-v3) describes the features the API gained. This page is the other half of the story: the behaviour that changed underneath a client you have already shipped. Work through it before you point an existing v2.x integration at a 3.0 installation. + +Nothing here changes an endpoint path, the OAuth flow, or the product and category payload structure. The work is in permissions, error handling, request pacing, and pagination bookkeeping. + +## What Changes for an Existing Client + +| Change | Symptom if you ignore it | Section | +|---------------------------------------|------------------------------------------------|---------| +| Permissions enforced on reads too | `403` on calls that worked before | [Permissions](#permissions-are-now-enforced-on-every-request) | +| Rate limits enforced | `429` mid-sync, or token endpoint locked out | [Pace Your Requests](#pace-your-requests) | +| `limit` capped at 100 | Sync silently stops after the first page | [Pagination](#pagination-bookkeeping) | +| Single error envelope | Error messages parsed from the wrong keys | [Error Responses](#error-responses-have-a-single-shape) | +| `Accept` header enforced | `406` on every request | [Headers](#headers-every-request-sends) | +| `configrable-products` deprecated | Works today, breaks on a later release | [Deprecated Alias](#the-deprecated-product-alias) | + +Everything else on this page is additive — new data you may read, not changes you must absorb. + +## Headers Every Request Sends + +| Key | Value | +| ------------- | --------------------- | +| Accept | application/json | +| Authorization | Bearer `access_token` | + +`Accept: application/json` is now mandatory. A request without it is rejected before it reaches the controller: + +```json +{ + "error": "Accept header must be application/json" +} +``` + +That response carries `406 Not Acceptable`. Many HTTP clients send `Accept: */*` by default, which passes, but set the header explicitly rather than relying on it. + +## Permissions Are Now Enforced on Every Request + +In v2.x, an endpoint with no entry in the API access-control map was reachable by any authenticated key as long as the request was a read — only writes were checked. In 3.0 the check applies to every request, in both directions: + +- A route the key is not granted returns `403`, whether it is a `GET` or a `DELETE`. +- A revoked API key returns `403`, even while its access token is still within its lifetime. +- A key whose API user has been disabled returns `403` for the same reason. If that user is disabled before you authenticate, the token request itself fails with `400` and an `invalid_grant` error rather than issuing a token. + +Two consequences follow for an existing integration. + +First, a key created with **Custom** permissions loses access to endpoints it could previously read. Grant the missing permissions on the integration, or switch the key to **All**. Keys already set to **All** are unaffected. + +Second, the permissions your client needs belong in your own installation instructions. A `403` is no longer a sign that something is broken — it is a sign that a checkbox is unticked. + +::: tip Verify permissions before you sync +Call one endpoint per capability your client uses at startup, and report a `403` to the merchant naming the call. Discovering a missing permission on the first request is far cheaper than discovering it halfway through a catalog import. +::: + +### Permissions Behind the 3.0 Endpoints + +The endpoints introduced in 3.0 are governed by these permission keys. Where a key already existed in v2.x, the new verb simply joins it: + +| Endpoints | Permission key | +|---|---| +| `PATCH`, `DELETE` on attributes, and delete attribute option | `api.catalog.attributes.edit`, `api.catalog.attributes.delete` | +| `PATCH`, `DELETE` on attribute groups | `api.catalog.attribute_groups.edit`, `api.catalog.attribute_groups.delete` | +| `PATCH`, `DELETE` on families | `api.catalog.families.edit`, `api.catalog.families.delete` | +| `PATCH`, `DELETE` on category fields, and delete field option | `api.catalog.category_fields.edit`, `api.catalog.category_fields.delete` | +| `PATCH` on categories | `api.catalog.categories.edit` | +| `PATCH` on products and configurable products | `api.catalog.products.edit` | +| `GET`, `DELETE` product media | `api.catalog.products`, `api.catalog.products.delete` | +| `GET`, `DELETE` category media | `api.catalog.categories`, `api.catalog.categories.delete` | +| `GET`, `DELETE` swatch media | `api.catalog.attributes`, `api.catalog.attributes.delete` | +| Locale, channel, currency writes | `api.settings.locales.*`, `api.settings.channels.*`, `api.settings.currencies.*` (`create`, `edit`, `delete`) | +| Passport read and lifecycle | `api.catalog.passports`, `api.catalog.passports.publish`, `api.catalog.passports.withdraw` | +| Measurement families and units | `api.catalog.measurements`, `api.catalog.measurements.units`, each with `create`, `edit`, `delete` | + +Reinstating a passport is governed by the publish permission, and redacting one by the withdraw permission, since each is the same class of action. + +## Error Responses Have a Single Shape + +Every failure now returns the same envelope, so a client can parse one structure instead of matching on message text. A validation failure carries a per-field `errors` object: + +```json +{ + "success": false, + "message": "Validation failed.", + "errors": { + "values.common.sku": ["The values.common.sku field is required."] + } +} +``` + +Everything else — a missing record, a permission failure, a rejected filter — carries `success` and `message` alone: + +```json +{ + "success": false, + "message": "This action is unauthorized" +} +``` + +These are the status codes a client should handle explicitly: + +| Status | Meaning | What the client should do | +|--------|-------------------------------------------------|--------------------------------------------------| +| `401` | Token missing, expired, or revoked | Re-authenticate, then replay the request once | +| `403` | The key lacks the permission for that endpoint | Surface which call failed; do not retry | +| `404` | No record for that code or SKU | Treat as absent, not as an outage | +| `406` | `Accept` header is not `application/json` | Fix the client's headers | +| `422` | Validation or filter error | Read `errors` and report per field | +| `429` | Rate limit exceeded | Back off and retry; see below | + +Success responses keep the shape they had in v2.x: `success`, `message`, and an optional `data` key. Creates return `201`, updates and deletes `200`, and a passport publish returns `202` because the work is queued. + +## Pace Your Requests + +3.0 enforces rate limits: **120 requests per minute** across the API, and **10 per minute** against token issue and refresh. A client that fetches a fresh token before every call exhausts the token limit almost immediately. + +Two changes cover it. + +**Cache the access token.** Request it once, keep it for its lifetime — one hour by default, and the exact value comes back as `expires_in` — and only re-authenticate when it expires or a call returns `401`: + +``` +if (token is null or token expires within 60 seconds) { + token = POST /oauth/token (grant_type=refresh_token, or password on first run) +} +``` + +**Back off on `429`.** Read the `Retry-After` header, wait that long, and retry with an exponential backoff instead of failing the whole sync: + +``` +attempt = 0 +while (attempt < 5) { + response = send(request) + if (response.status != 429) return response + wait(response.header("Retry-After") ?? 2 ** attempt) + attempt++ +} +``` + +Both limits are set per installation, so a merchant on a dedicated instance may have more headroom. Write the client against the defaults regardless — you cannot know which installation it will run on. + +### Cheap Polling + +If you poll frequently, adopt conditional requests. Every response carries an `ETag`; send it back in `If-None-Match` and a `304 Not Modified` tells you nothing changed, with no body to re-process: + +``` +GET {{url}}/api/v1/rest/attributes +If-None-Match: "a3f1c8..." +``` + +::: warning Do not treat 304 as an empty result +A `304` means *unchanged*, not *no records*. A client that maps it onto an empty collection will look like a catalog that emptied itself. Handle `304` explicitly, or leave `If-None-Match` off entirely. +::: + +One caveat when writing and reading back: structure resources — attributes, attribute groups, families, category fields, locales, channels, currencies — are served from a server-side cache. It invalidates on writes and on import completion, but an immediate read-back is not how you confirm a write succeeded; the write's own response already told you. + +## Pagination Bookkeeping + +The `limit` parameter is clamped to a maximum of **100** and defaults to 10. A request for `limit=500` returns 100 records and no error. + +::: danger The silent truncation +A loop that stops when a page is smaller than the page size it asked for will stop after the first page and report a successful, partial sync. This is the single most likely way a working v2.x client breaks against 3.0. +::: + +Drive the loop from the response instead: + +``` +url = "{{url}}/api/v1/rest/products?limit=100" +while (url != null) { + response = get(url) + process(response.data) + url = response.links.next +} +``` + +This works in both modes. In page mode, `links.next` is `null` on the last page. In cursor mode (`pagination_type=search_after`), `links.next` is `null` and `search_after` comes back as `null` when the catalog is exhausted. + +Cursor responses carry no `meta.total` and no `meta.last_page` — avoiding the `COUNT(*)` those fields require is the point of the mode — so a progress indicator has to come from your own record count. + +For a full catalog pull or a scheduled delta sync, cursor mode is the mode to choose: + +``` +GET {{url}}/api/v1/rest/products + ?filters={"updated_at":[{"operator":">=","value":"2026-08-01 00:00:00"}]} + &pagination_type=search_after + &limit=100 +``` + +Products also filter on `sku`, `status`, `family`, and `categories`. See [Delta Synchronization](./whats-new-v3#delta-synchronization) for the full operator list. + +## Product Associations + +A single product `GET` now returns an `associations` block alongside the existing payload. It covers every association type the installation defines, including custom ones, and carries each link's `additional_data`: + +```json +{ + "associations": { + "related": [ + { "related_sku": "shirt-2", "additional_data": null } + ], + "spare_parts": [ + { "related_sku": "filter-9", "additional_data": { "quantity": 2 } } + ] + } +} +``` + +This is additive. The `values.associations` SKU lists a v2.x client already reads are unchanged, and the listing endpoint does not include the block at all — it is returned only for a single product, to keep list responses free of a per-row query. + +The same structure may be sent on create and update, under a top-level `associations` key: + +```json +{ + "associations": { + "spare_parts": [ + { "sku": "filter-9", "additional_data": { "quantity": 2 } } + ] + } +} +``` + +How it resolves: + +- Each type you submit replaces that type's links entirely; types you omit are left alone. +- `additional_data` is validated against the custom fields defined on that association type. An invalid value fails the whole request with `422` before anything is written. +- A SKU that does not resolve is skipped rather than failing the request. +- A product cannot be associated with itself; such a link is dropped. + +## The Deprecated Product Alias + +The misspelled `configrable-products` prefix still works and still resolves to the same controller, but it now returns RFC 8594 deprecation headers: + +``` +Deprecation: true +Link: ; rel="successor-version" +``` + +Change the prefix to `configurable-products` in your client. The alias will be removed in a future release; until then its permission keys mirror the correctly spelled route. + +## Migration Checklist + +| Change | Required | +|---|---| +| Send `Accept: application/json` on every request | Yes | +| Cache the access token; back off on `429` | Yes | +| Follow `links.next` instead of comparing page size to `limit` | Yes, if you request more than 100 per page | +| Parse the `errors` object on `422`; treat `403` as a missing permission | Yes | +| Handle `304`, or stop sending `If-None-Match` | Yes, if you send conditional requests | +| Re-check Custom permissions on the integration | Yes, for keys not set to All | +| Rename `configrable-products` to `configurable-products` | Yes | +| Move a full re-sync to `updated_at` filters with cursor pagination | Recommended | +| Read `associations` and `additional_data` for rich product links | Optional | diff --git a/src/3.0/api/passports.md b/src/3.0/api/passports.md new file mode 100644 index 00000000..4e0d51bc --- /dev/null +++ b/src/3.0/api/passports.md @@ -0,0 +1,139 @@ +# Digital Product Passports + +Digital Product Passports are published per product, channel, and locale, each publication being an immutable version with a public URL. This page covers the REST lifecycle: listing and reading publications, publishing new ones, and withdrawing, reinstating, or redacting existing ones. For templates, carriers, and the admin workflow, see [Digital Product Passport](../advanced/digital-product-passport). + +::: warning Feature-gated +Every endpoint here returns `404` while Digital Product Passports are disabled for the installation. Enable the feature in the System Settings hub first. +::: + +### Common Headers + +Every request on this page sends the same two headers: + +| Key | Value | +| ------------- | --------------------- | +| Accept | application/json | +| Authorization | Bearer `access_token` | + +## List Publications + +Returns publications newest first. + +``` +GET {{url}}/api/v1/rest/passports +``` + +| Name | Info | Type | Default | +|----------|----------------------------------------------------------|--------|---------| +| `limit` | Records per request. Clamped to `1`–`100` | Number | `10` | +| `sku` | Only publications belonging to this product | String | N/A | +| `status` | Only publications in this status | String | N/A | + +### Response + +::: details Response +```json +{ + "data": [ + { + "uuid": "0f0f7a5c-6d1b-4a49-9a2e-7b5f2c9d1e34", + "status": "published", + "type": "dpp", + "product_sku": "SHIRT-1", + "channel": "ecommerce", + "gtin": "04012345678901", + "gs1_link": "/01/04012345678901", + "public_url": "https://example.com/p/0f0f7a5c-6d1b-4a49-9a2e-7b5f2c9d1e34", + "published_at": "2026-08-01T09:12:44+00:00" + } + ], + "links": { "first": "…", "last": "…", "prev": null, "next": null }, + "meta": { "current_page": 1, "per_page": 10, "total": 1 } +} +``` +::: + +## Read a Product's Publications + +Every publication for one product, newest first, unpaginated. + +``` +GET {{url}}/api/v1/rest/passports/{sku} +``` + +An unknown SKU returns `404`. + +## Publish + +Queues publication for a product on one channel and one or more locales. + +``` +POST {{url}}/api/v1/rest/passports/publish/{sku} +``` + +```json +{ + "channel_id": 1, + "locale_ids": [1, 3] +} +``` + +| Field | Required | Notes | +|---|---|---| +| `channel_id` | Yes | Must be an existing channel id. | +| `locale_ids` | Yes | Non-empty array of existing locale ids. | + +### Response + +The work runs on the `publication` queue, so the endpoint answers `202 Accepted` rather than waiting: + +```json +{ + "success": true, + "message": "Passport publication has been queued." +} +``` + +Make sure a worker is processing the `publication` queue, or nothing is published — see [Queue Management](../advanced/queue-management). + +## Withdraw + +Takes a published passport off the public URL. The version is retained. + +``` +POST {{url}}/api/v1/rest/passports/withdraw/{id} +``` + +`{id}` is the publication's numeric id. An unknown id returns `404`. + +## Reinstate + +Puts a withdrawn passport back online. + +``` +POST {{url}}/api/v1/rest/passports/reinstate/{id} +``` + +## Redact + +Permanently removes the payload of a publication for a GDPR request, leaving a tombstone at the public URL. + +``` +POST {{url}}/api/v1/rest/passports/redact/{id} +``` + +```json +{ + "reason": "Data subject erasure request #4182" +} +``` + +`reason` is required and stored with the redaction, up to 1000 characters. + +## Permissions + +| Endpoint | Permission key | +|---|---| +| List and read | `api.catalog.passports` | +| Publish, reinstate | `api.catalog.passports.publish` | +| Withdraw, redact | `api.catalog.passports.withdraw` | diff --git a/src/3.0/api/product.md b/src/3.0/api/product.md index 68121ec5..4b9fff64 100644 --- a/src/3.0/api/product.md +++ b/src/3.0/api/product.md @@ -23,12 +23,14 @@ GET {{url}}/api/v1/rest/products You can shape the result set with these query parameters: -| Name | Info | Type | Default | -|---------------------|-------------------------------------------------|---------|---------| -| `limit` | The number of products to retrieve per request | Number | `10` | -| `page` | Page number to retrieve | Number | `1` | -| `filters` | Criteria to filter the records returned | JSON | N/A | -| `with_completeness` | Returns completeness scores for the product | Boolean | false | +| Name | Info | Type | Default | +|---------------------|-------------------------------------------------------------------|---------|---------| +| `limit` | Products per request. Clamped to a maximum of `100` | Number | `10` | +| `page` | Page number to retrieve (page mode only) | Number | `1` | +| `filters` | Criteria to filter the records returned | JSON | N/A | +| `with_completeness` | Returns completeness scores for the product | Boolean | `false` | +| `pagination_type` | `page` (default) or `search_after` for cursor pagination | String | `page` | +| `search_after` | Cursor from the previous cursor-mode response | Number | N/A | ### Usage Examples @@ -79,6 +81,11 @@ You can shape the result set with these query parameters: - `IN`: Matches any of the family types in the provided list. - `NOT IN`: Excludes any of the family types in the provided list. + 6. **updated_at** and **created_at** + - **Operators:** `>`, `>=`, `<`, `<=`, and `BETWEEN` (which expects exactly two values). + - Values are date strings, for example `2026-08-01 00:00:00`. An unparseable value returns `422`. + - These combine with AND, so a delta filter always narrows the result set. Pair them with `pagination_type=search_after` for incremental syncs — see [Delta Synchronization](./whats-new-v3#delta-synchronization). + #### Example Usage - **Filter by SKU:** @@ -315,6 +322,39 @@ The full product record is returned: ``` ::: +## Product Associations + +A single-product `GET` returns an extra `associations` block alongside `values`. It covers every association type the installation defines — the three built-in sections and any custom type — and carries each link's `additional_data`: + +```json +{ + "associations": { + "related_products": [ + { "related_sku": "100PS", "additional_data": null } + ], + "spare_parts": [ + { "related_sku": "FILTER-9", "additional_data": { "quantity": 2 } } + ] + } +} +``` + +The block is returned only for a single product, never on the listing, so a paginated response does not run one query per row. The legacy `values.associations` SKU lists are unchanged. + +You may send the same structure when creating or updating a product, under a top-level `associations` key: + +```json +{ + "associations": { + "spare_parts": [ + { "sku": "FILTER-9", "additional_data": { "quantity": 2 } } + ] + } +} +``` + +Each type you submit replaces that type's links entirely, and types you omit are left alone. `additional_data` is validated against the fields defined on that association type — an invalid value fails the request with `422` before anything is written. A SKU that does not resolve is skipped, and a product cannot be associated with itself. See [Configurable Associations](../packages/configurable-associations) for defining types. + ## Create a Product Creates a new simple product with the SKU, family, and attribute values you supply. diff --git a/src/3.0/api/variant_structures.md b/src/3.0/api/variant_structures.md new file mode 100644 index 00000000..ae16c982 --- /dev/null +++ b/src/3.0/api/variant_structures.md @@ -0,0 +1,122 @@ +# Variant Structures + +A variant structure defines how a family's configurable products are built: which attributes act as variant axes, over one or two levels, and at which level each attribute's value is stored. This page covers managing structures over the REST API. For the concept, see [Advanced Variants](../packages/advanced-variants). + +Structures always belong to an attribute family, so every route is nested under the family code. + +### Common Headers + +Every request on this page sends the same two headers: + +| Key | Value | +| ------------- | --------------------- | +| Accept | application/json | +| Authorization | Bearer `access_token` | + +## Get All Structures for a Family + +``` +GET {{url}}/api/v1/rest/families/{code}/variant-structures +``` + +**Headers** — use the [Common Headers](#common-headers). + +| Name | Info | Type | Default | +|---------|----------------------------------------------------|--------|---------| +| `limit` | Records per request. Clamped to a maximum of `100` | Number | `10` | +| `page` | Page number to retrieve | Number | `1` | + +### Response + +::: details Response +```json +{ + "data": [ + { + "code": "colour_size", + "name": "Colour then Size", + "family": "apparel", + "levels": 2, + "axes": { + "level_1": ["colour"], + "level_2": ["size"] + }, + "placements": { + "common": ["brand"], + "sub_parent": ["colour_image"], + "variant": ["sku", "size"] + }, + "effective_placements": { + "common": ["brand"], + "sub_parent": ["colour_image"], + "variant": ["sku", "size"] + }, + "created_at": "2026-07-22T10:14:03.000000Z", + "updated_at": "2026-07-22T10:14:03.000000Z" + } + ] +} +``` +::: + +Both axis levels are always present — `level_2` comes back as an empty array for a single-level structure — so a client never has to branch on a missing key, and a `GET` result round-trips unchanged as a `PUT` body. + +`placements` is what you configured; `effective_placements` is what the resolver actually applies once family defaults are taken into account. + +## Get a Structure by Code + +``` +GET {{url}}/api/v1/rest/families/{code}/variant-structures/{structureCode} +``` + +## Create a Structure + +``` +POST {{url}}/api/v1/rest/families/{code}/variant-structures +``` + +```json +{ + "code": "colour_size", + "name": "Colour then Size", + "levels": 2, + "axes": { + "level_1": ["colour"], + "level_2": ["size"] + }, + "placements": { + "common": ["brand"], + "sub_parent": ["colour_image"], + "variant": ["sku", "size"] + } +} +``` + +| Field | Required | Notes | +|---|---|---| +| `code` | Yes | Must pass the standard code rule. | +| `name` | No | Display name. | +| `levels` | Yes | `1` or `2`. Only settable at creation. | +| `axes` | Yes | Keys `level_1` (required) and `level_2`; values are attribute codes. | +| `placements` | No | Keys `common`, `sub_parent`, `variant`; values are attribute codes. | + +::: warning Levels and axes are immutable after creation +Creation is the only point at which `levels` and `axes` may be stated. The update verbs accept them only as unchanged values — a structure's shape cannot be rewritten once products are built on it. +::: + +## Update a Structure + +``` +PUT {{url}}/api/v1/rest/families/{code}/variant-structures/{structureCode} +PATCH {{url}}/api/v1/rest/families/{code}/variant-structures/{structureCode} +``` + +Use these to rename a structure or adjust `placements`. + +## Delete a Structure + +``` +DELETE {{url}}/api/v1/rest/families/{code}/variant-structures/{structureCode} +``` + +A structure that products already point at cannot be deleted; the request is refused with a `422`. diff --git a/src/3.0/api/whats-new-v3.md b/src/3.0/api/whats-new-v3.md index 68512a40..b8311b6c 100644 --- a/src/3.0/api/whats-new-v3.md +++ b/src/3.0/api/whats-new-v3.md @@ -8,6 +8,8 @@ If you are building or maintaining an API client, this page is your tour of ever We will start with the new write operations and media endpoints, move on to delta synchronization, and finish with the authentication changes and a deprecation you should plan for. +If you are updating a client that already runs against v2.x, read [Migrating an API Client to v3.0](./migrating-your-client) as well — it covers the behaviour that changed underneath an existing integration: permission enforcement, error shapes, rate limits, and pagination bookkeeping. + ## New Write Operations Catalog-structure resources gained the verbs they were missing. The table below lists each resource and the operations added in 3.0: @@ -37,12 +39,16 @@ GET /api/v1/rest/media-files/swatch?code=red&attribute_code=color ## Digital Product Passports -Passports are fully manageable over the API under `/api/v1/rest/passports` — you may list them, read them per SKU, publish (the endpoint returns `202` and queues the work), withdraw, reinstate, and perform a GDPR redact. See [Digital Product Passport](../advanced/digital-product-passport#rest-api). +Passports are fully manageable over the API under `/api/v1/rest/passports` — you may list them, read them per SKU, publish (the endpoint returns `202` and queues the work), withdraw, reinstate, and perform a GDPR redact. See [Digital Product Passports](./passports). ## Measurements Measurement families, units, and attribute bindings are fully manageable over the API — see [Measurements](../packages/measurements#rest-api). +## Association Types and Variant Structures + +Custom association types and their per-link fields are managed at `/api/v1/rest/association-types`, and a family's variant structures at `/api/v1/rest/families/{code}/variant-structures`. See [Association Types](./association_types) and [Variant Structures](./variant_structures). + ## Delta Synchronization Sometimes you may wish to sync only the products that changed since your last run rather than paging through the entire catalog. Products now support exactly that, combining date filters with cursor pagination: diff --git a/src/3.0/architecture/frontend.md b/src/3.0/architecture/frontend.md index c7868c2f..416bb3cd 100644 --- a/src/3.0/architecture/frontend.md +++ b/src/3.0/architecture/frontend.md @@ -27,7 +27,7 @@ UnoPim leverages the Blade template engine, integrated with [Laravel](https://la - **Template Inheritance**: Blade allows a modular structure through template inheritance. - **Directives**: Blade simplifies common tasks like loops and conditionals with its built-in directives. -For more details on UnoPim's directory structure and configuration, visit the [official documentation](https://devdocs.unopim.com/master/packages/views.html#directory-structure). +For more details on UnoPim's directory structure and configuration, see [Views](../packages/views#directory-structure). ## AJAX Navigation diff --git a/src/3.0/architecture/index.md b/src/3.0/architecture/index.md index 42f7bd58..13ad35e4 100644 --- a/src/3.0/architecture/index.md +++ b/src/3.0/architecture/index.md @@ -2,11 +2,11 @@ **UnoPim** is designed to be an intuitive and easy-to-understand platform. This document provides an overview of the architecture and how UnoPim works. -UnoPim v3.0 is built on top of popular open-source technologies such as [PHP 8.4+](https://php.net), [Laravel 13](https://laravel.com), [Vue.js 3](https://vuejs.org/), and [Tailwind CSS 3](https://tailwindcss.com/), making it a modern, scalable, and flexible PIM solution. The platform also integrates AI capabilities through the **MagicAI** and **AiAgent** packages, enabling intelligent content generation, product enrichment, and conversational PIM management powered by 10+ AI providers. +UnoPim is built on top of popular open-source technologies such as [PHP 8.4+](https://php.net), [Laravel 13](https://laravel.com), [Vue.js 3](https://vuejs.org/), and [Tailwind CSS 3](https://tailwindcss.com/), making it a modern, scalable, and flexible PIM solution. The platform also integrates AI capabilities through the **MagicAI** and **AiAgent** packages, enabling intelligent content generation, product enrichment, and conversational PIM management powered by 10+ AI providers. As **UnoPim** is tailored for Product Information Management (PIM) needs, it provides both front-end and back-end features that enable businesses to manage product data efficiently while allowing for comprehensive administrative control. -The architecture is highly modular, with Laravel packages separating each core functionality such as Categories, Products, AI-powered enrichment, and other essential features. This separation of concerns allows for easier customization and extension of the platform. UnoPim v2.0.0 introduced the **AiAgent** package, which provides a conversational AI interface with 32+ PIM-specific tools, and enhances the **MagicAI** package with multi-platform AI provider support and database-backed credential management. +The architecture is highly modular, with Laravel packages separating each core functionality such as Categories, Products, AI-powered enrichment, and other essential features. This separation of concerns allows for easier customization and extension of the platform. The **AiAgent** package provides a conversational AI interface with 35 PIM-specific tools, and the **MagicAI** package adds multi-platform AI provider support with database-backed credential management. **UnoPim** integrates Vue.js for building dynamic user interfaces, leveraging built-in components to enhance user experience and responsiveness. diff --git a/src/3.0/architecture/packages.md b/src/3.0/architecture/packages.md index 399f7f8e..c3f8b621 100644 --- a/src/3.0/architecture/packages.md +++ b/src/3.0/architecture/packages.md @@ -44,6 +44,24 @@ The **AdminApi** package in UnoPim provides API functionalities for managing adm - **Configuration and Settings** - API access to platform settings, including languages, locales, and customization options. +### AiAgent + +The **AiAgent** package provides an AI-powered conversational interface for managing PIM operations through natural language. It enables administrators to interact with product data, run enrichment tasks, and monitor data quality using an intelligent chat-based workflow. + +#### Key Features of the AiAgent Package + +* **AI Agent Chat** with 35 PIM-specific tools for product management, category operations, attribute handling, and data analysis +* **Approval Queue** for reviewing and approving AI-suggested changes before they are applied to product data +* **Auto-Enrichment** capabilities for automatically generating and improving product descriptions, translations, and attribute values +* **Content Feedback** system allowing users to rate and refine AI-generated content +* **Memory System** that retains conversation context and user preferences across sessions +* **Quality Monitor** for tracking product data quality scores and identifying areas for improvement +* **Token Tracking** to monitor AI usage and costs across providers and sessions + +### AppUrlGuard + +The **AppUrlGuard** package detects a mismatch between the configured `APP_URL` and the address the admin is actually being served from, and shows an interstitial rather than letting the panel load with broken asset and route URLs. + ### Attribute The **Attribute** package in UnoPim handles all logic related to product attributes and attribute sets. This package allows you to define and organize product information effectively and customize data to enhance search and filtering capabilities. @@ -98,11 +116,11 @@ The **Core** package serves as the foundation for various functionalities and ut - Provides utilities for tasks such as data manipulation, file handling, and date/time formatting. - Includes validation functions for input data, ensuring data integrity. -### Datagrid +### DataGrid -The **Datagrid** package provides a solution for displaying and managing tabular data within the admin panel. It enables efficient data handling and enhances the user experience with configurable columns, filters, and sorting options. +The **DataGrid** package provides a solution for displaying and managing tabular data within the admin panel. It enables efficient data handling and enhances the user experience with configurable columns, filters, and sorting options. -#### Key Features of the Datagrid Package +#### Key Features of the DataGrid Package - **Dynamic Data Presentation** - Allows administrators to configure columns, filters, and sorting for displaying product data in tables. @@ -114,7 +132,7 @@ The **Datagrid** package provides a solution for displaying and managing tabular ### DataTransfer -The **DataTransfer** package manages data imports and exports, facilitating bulk operations such as importing large volumes of product information. For more details, see the [DataTransfer documentation](/2.1/packages/data-transfer). +The **DataTransfer** package manages data imports and exports, facilitating bulk operations such as importing large volumes of product information. For more details, see the [Data Transfer documentation](../packages/data-transfer). ### DebugBar @@ -124,10 +142,6 @@ The **DebugBar** package includes essential tools for monitoring, analyzing, and The **ElasticSearch** package integrates ElasticSearch functionalities into UnoPim, enabling advanced search capabilities and data indexing. It allows for efficient querying and retrieval of product information, enhancing the overall search experience. -### FPC - -The **FPC (Full Page Cache)** package optimizes the platform’s performance by caching generated pages, reducing server load, and improving response times for administrators. - ### HistoryControl The **HistoryControl** package keeps track of changes made to product data and other critical information, allowing administrators to monitor revisions and revert to previous versions if necessary. @@ -149,19 +163,9 @@ The **MagicAI** package integrates AI-based functionalities into UnoPim, offerin * **Database-backed credential management** for securely storing and managing API keys and provider configurations per platform * Platform-specific model discovery and validation -### AiAgent - -The **AiAgent** package provides an AI-powered conversational interface for managing PIM operations through natural language. It enables administrators to interact with product data, run enrichment tasks, and monitor data quality using an intelligent chat-based workflow. +### Measurement -#### Key Features of the AiAgent Package - -* **AI Agent Chat** with 32+ PIM-specific tools for product management, category operations, attribute handling, and data analysis -* **Approval Queue** for reviewing and approving AI-suggested changes before they are applied to product data -* **Auto-Enrichment** capabilities for automatically generating and improving product descriptions, translations, and attribute values -* **Content Feedback** system allowing users to rate and refine AI-generated content -* **Memory System** that retains conversation context and user preferences across sessions -* **Quality Monitor** for tracking product data quality scores and identifying areas for improvement -* **Token Tracking** to monitor AI usage and costs across providers and sessions +The **Measurement** package defines measurement families and their units, along with conversion rules and precision strategies, and binds them to measurement attributes. See [Measurements](../packages/measurements). ### Notification @@ -171,10 +175,26 @@ The **Notification** package manages system notifications and alerts, enabling a The **Product** package in UnoPim manages all essential product information, including attributes, variants, and categorization. It allows administrators to create, update, and organize product data efficiently, supporting advanced configurations and real-time updates. +### ProductPassport + +The **ProductPassport** package builds Digital Product Passports on top of Publication — admin-editable templates bound to attribute families, access tiers, QR carriers, and GS1 Digital Link aliases. See [Digital Product Passport](../advanced/digital-product-passport). + +### Publication + +The **Publication** package is the generic publishing engine: config-registered publication types, append-only per-locale versions, public rendering with ETags and tombstones, and the jobs that publish, withdraw, reinstate, and redact. + +### Resource + +The **Resource** package provides the CRUD kit for package developers: base controllers and reusable DataGrid and edit components that scaffold a full admin section with very little code. See [Resource CRUD Kit](../packages/resource-crud-kit). + +### Theme + +The **Theme** package holds the theme engine and view composition layer that lets packages register and override admin views. + ### User The **User** package handles user management, including roles, permissions, and profiles for administrators, ensuring secure access and personalized experiences within the platform. ### Webhook -The **Webhook** package manages product webhook configurations, including enabling or disabling webhook and handling product webhooks triggered by any product changes. +The **Webhook** package manages webhook endpoints and their event subscriptions, signs deliveries with HMAC, and records a delivery log per endpoint. See [Webhooks](../packages/webhooks). diff --git a/src/3.0/architecture/repository-pattern.md b/src/3.0/architecture/repository-pattern.md index c91f8e79..6ae0df66 100644 --- a/src/3.0/architecture/repository-pattern.md +++ b/src/3.0/architecture/repository-pattern.md @@ -20,4 +20,4 @@ By using the Repository Pattern with Prettus, UnoPim ensures a more structured a ## Eloquent ORM -[Eloquent](https://laravel.com/docs/10.x/eloquent), the ORM in **Laravel**, simplifies database operations by letting developers work with objects instead of writing SQL queries, making data manipulation more intuitive. \ No newline at end of file +[Eloquent](https://laravel.com/docs/13.x/eloquent), the ORM in **Laravel**, simplifies database operations by letting developers work with objects instead of writing SQL queries, making data manipulation more intuitive. \ No newline at end of file diff --git a/src/3.0/introduction/index.md b/src/3.0/introduction/index.md index 80def2e4..f5acb388 100644 --- a/src/3.0/introduction/index.md +++ b/src/3.0/introduction/index.md @@ -5,7 +5,7 @@ UnoPim is a leading open-source Product Information Management (PIM) platform built on Laravel 13, PHP 8.4, Vue 3, and Tailwind CSS. It offers a comprehensive set of tools to manage product data efficiently across multiple channels and locales. UnoPim is actively developed on [GitHub](https://github.com/unopim/unopim). ::: tip What's new in v3.0 -v3.0 introduces Digital Product Passports, configurable association types, advanced product variants, measurement families with unit conversions, Microsoft SSO (Entra ID), multi-webhooks, and a modernized admin with AJAX navigation and dark mode. See [What's New in v3.0](../api/whats-new-v3) for the full list. Upgrading from v2.x? See the [Upgrade Guide](../prologue/upgrade-guide). +v3.0 introduces Digital Product Passports, configurable association types, two-level variant structures, measurement families and units, multi-webhooks, Microsoft SSO (Entra ID), and a greatly expanded REST API — on Laravel 13 and PHP 8.4. See [What's New in v3.0](../api/whats-new-v3) for the full list. Upgrading from v2.1.x? See the [Upgrade Guide](../prologue/upgrade-guide). ::: ## Key Features of UnoPim @@ -30,7 +30,7 @@ UnoPim streamlines the management of product information, allowing businesses to ### AI-Powered Enrichment -UnoPim 3.0 ships with an integrated AI Agent that exposes 32+ PIM-specific tools through a chat-style interface, plus the **MagicAI** content engine which supports 10+ providers (OpenAI, Gemini, Groq, Ollama, Anthropic, and more) for generation and translation of product attribute values across locales. +UnoPim 3.0 ships with an integrated AI Agent that exposes 35 PIM-specific tools through a chat-style interface, plus the **MagicAI** content engine which supports 10+ providers (OpenAI, Anthropic, Gemini, Groq, Ollama, Mistral, DeepSeek, Azure, xAI, OpenRouter, and any OpenAI-compatible endpoint) for generation and translation of product attribute values across locales. ### Swatch Attributes and Dashboard Insights diff --git a/src/3.0/introduction/installation-centos.md b/src/3.0/introduction/installation-centos.md index 619f5a1d..0b5b6602 100644 --- a/src/3.0/introduction/installation-centos.md +++ b/src/3.0/introduction/installation-centos.md @@ -60,8 +60,8 @@ Update the following values: ```ini memory_limit = 512M max_execution_time = 120 -upload_max_filesize = 50M -post_max_size = 50M +upload_max_filesize = 200M +post_max_size = 200M date.timezone = UTC ``` @@ -335,8 +335,7 @@ SESSION_DRIVER=redis REDIS_HOST=127.0.0.1 REDIS_PORT=6379 -ELASTICSEARCH_HOST=127.0.0.1 -ELASTICSEARCH_PORT=9200 +ELASTICSEARCH_HOST=127.0.0.1:9200 ``` ### Run the Installer @@ -394,7 +393,17 @@ server { gzip_comp_level 5; # Static file caching - location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ { + # Dynamic routes rendered by PHP — these must precede the static rule + # below, which would otherwise 404 them. + location ^~ /cache/ { + try_files $uri /index.php?$query_string; + } + + location ~ ^/p/[^/]+/carrier\.svg$ { + try_files $uri /index.php?$query_string; + } + + location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|webp|map|woff|woff2|ttf|eot)$ { expires 30d; add_header Cache-Control "public, immutable"; try_files $uri =404; @@ -422,6 +431,11 @@ server { deny all; } + # Never serve executable or active content from uploads + location ~* ^/storage/.*\.(php[0-9]?|phtml|pht|phar|html?|shtml|xhtml)$ { + return 404; + } + # Deny PHP execution in writable directories location ~* ^/(storage|bootstrap/cache)/.*\.php$ { deny all; diff --git a/src/3.0/introduction/installation-debian.md b/src/3.0/introduction/installation-debian.md index 8d3bffdb..29934006 100644 --- a/src/3.0/introduction/installation-debian.md +++ b/src/3.0/introduction/installation-debian.md @@ -62,8 +62,8 @@ Update the following values: ```ini memory_limit = 512M max_execution_time = 120 -upload_max_filesize = 50M -post_max_size = 50M +upload_max_filesize = 200M +post_max_size = 200M date.timezone = UTC ``` @@ -293,8 +293,7 @@ SESSION_DRIVER=redis REDIS_HOST=127.0.0.1 REDIS_PORT=6379 -ELASTICSEARCH_HOST=127.0.0.1 -ELASTICSEARCH_PORT=9200 +ELASTICSEARCH_HOST=127.0.0.1:9200 ``` ### Run the Installer @@ -348,7 +347,17 @@ server { gzip_comp_level 5; # Static file caching - location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ { + # Dynamic routes rendered by PHP — these must precede the static rule + # below, which would otherwise 404 them. + location ^~ /cache/ { + try_files $uri /index.php?$query_string; + } + + location ~ ^/p/[^/]+/carrier\.svg$ { + try_files $uri /index.php?$query_string; + } + + location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|webp|map|woff|woff2|ttf|eot)$ { expires 30d; add_header Cache-Control "public, immutable"; try_files $uri =404; @@ -376,6 +385,11 @@ server { deny all; } + # Never serve executable or active content from uploads + location ~* ^/storage/.*\.(php[0-9]?|phtml|pht|phar|html?|shtml|xhtml)$ { + return 404; + } + # Deny PHP execution in writable directories location ~* ^/(storage|bootstrap/cache)/.*\.php$ { deny all; diff --git a/src/3.0/introduction/installation-docker.md b/src/3.0/introduction/installation-docker.md index d9284a28..1d3db8b3 100644 --- a/src/3.0/introduction/installation-docker.md +++ b/src/3.0/introduction/installation-docker.md @@ -47,12 +47,17 @@ Wait for the first-time setup (migrations and seeding) to complete, then open: http://localhost:8000/admin ``` -**Default Admin Credentials:** +**Admin credentials.** The first-run seeder creates `admin@example.com` and generates a **random 20-character password**, which it writes to `storage/app/admin-credentials.txt` inside the container: -| Field | Value | -|----------|---------------------| -| Email | `admin@example.com` | -| Password | `admin123` | +```bash +docker compose exec unopim cat storage/app/admin-credentials.txt +``` + +Log in, change the password, and delete the file. To choose the credentials yourself instead, set them before the first boot — they are only read while the `admins` table is still empty: + +```bash +INSTALLER_ADMIN_EMAIL=you@example.com INSTALLER_ADMIN_PASSWORD='a-strong-password' docker compose up -d +``` To change any setting, export the variable or drop it in a `.env` file next to `compose.yaml` — Compose interpolates it automatically: diff --git a/src/3.0/introduction/installation-ubuntu.md b/src/3.0/introduction/installation-ubuntu.md index 46499780..0aacdc25 100644 --- a/src/3.0/introduction/installation-ubuntu.md +++ b/src/3.0/introduction/installation-ubuntu.md @@ -317,7 +317,7 @@ DB_DATABASE=unopim DB_USERNAME=unopim DB_PASSWORD=your_secure_password -CACHE_DRIVER=redis +CACHE_STORE=redis QUEUE_CONNECTION=redis SESSION_DRIVER=redis @@ -326,8 +326,7 @@ REDIS_PORT=6379 ELASTICSEARCH_ENABLED=true ELASTICSEARCH_CONNECTION=default -ELASTICSEARCH_HOST=127.0.0.1 -ELASTICSEARCH_PORT=9200 +ELASTICSEARCH_HOST=127.0.0.1:9200 ``` Generate the application key: @@ -393,7 +392,17 @@ server { gzip_comp_level 5; # Static file caching - location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ { + # Dynamic routes rendered by PHP — these must precede the static rule + # below, which would otherwise 404 them. + location ^~ /cache/ { + try_files $uri /index.php?$query_string; + } + + location ~ ^/p/[^/]+/carrier\.svg$ { + try_files $uri /index.php?$query_string; + } + + location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|webp|map|woff|woff2|ttf|eot)$ { expires 30d; add_header Cache-Control "public, immutable"; try_files $uri =404; @@ -421,6 +430,11 @@ server { deny all; } + # Never serve executable or active content from uploads + location ~* ^/storage/.*\.(php[0-9]?|phtml|pht|phar|html?|shtml|xhtml)$ { + return 404; + } + # Deny PHP execution in writable directories location ~* ^/(storage|bootstrap/cache)/.*\.php$ { deny all; diff --git a/src/3.0/introduction/installation-with-postgresql.md b/src/3.0/introduction/installation-with-postgresql.md index 660d81f4..e4afa771 100644 --- a/src/3.0/introduction/installation-with-postgresql.md +++ b/src/3.0/introduction/installation-with-postgresql.md @@ -54,7 +54,7 @@ To install UnoPim using Composer, use the following steps: - If you have downloaded the zip file from the Git repository, extract the files into your desired directory and run the following command to set up the project: ```sh - composer create-project + composer install ``` - Otherwise, to directly install UnoPim, run the following command in your terminal: @@ -73,38 +73,45 @@ To install UnoPim using Composer, use the following steps: During the installation process, if the **`.env`** file doesn't exist, the installer will prompt you to provide the necessary information. ::: - ::: info Default Port for PostgreSql + ::: info Default port for PostgreSQL The default port for PostgreSQL is 5432. If you have configured a different port during installation, make sure to update it accordingly in env or when prompted for database port. ::: - Follow the prompts during the installation process to provide the following details: ``` - - Please Enter the APP URL : - - Please Enter the Application Name : - - Please select the default locale : - - Please enter the default currency : - - Please choose the Allowed Locales for your channels : - - Please choose the Allowed Currencies for your channels : - - Please select the Database Connection : - - Please enter the Database Host : - - Please enter the Database Port Number : - - Please enter the Database Name : - - Please enter the Database Prefix : - - Please enter the Database Username : - - Please enter the Database Password : + - Please provide the name of the application + - Please provide the application URL + - Please select the default application locale + - Please select the default currency + - Please choose the allowed locales for your channels + - Please choose the allowed currencies for your channels + - Please select the database connection + - Please enter the database host + - Please enter the database port + - Please enter the database name + - Please enter the database prefix + - Please enter your database username + - Please enter your database password + - Do you want to enable Elasticsearch? + (if yes: connection, host or Cloud ID, user, password or API key, index prefix) + - Select optional packages to install ``` ::: info Database Prefix (`DB_PREFIX`) The **Database Prefix** is optional. If you provide one, the installer validates and trims the value before applying it, so leading/trailing whitespace is removed automatically. As of UnoPim v2.1.0 this also prevents a double table prefix bug (for example tables being created as `wk_wk_channels`). Leave the prompt blank if you do not need a table prefix. ::: - - For Create your admin credentials: + - You will then be asked to create your admin credentials: ``` - - Enter the Name of Admin User : - - Enter the Email address of the Admin User : - - Configure the Password for admin user : + - Set the Name for Administrator + - Provide Email of Administrator + - Input a Password for Administrator ``` + + ::: tip Command options + `unopim:install` accepts `--skip-env-check`, `--skip-admin-creation`, `--with-demo-data`, and `--with-packages=` (comma-separated: `dam`, `shopify`, `bagisto`). + ::: ## Install Using GUI Installer To install UnoPim using our GUI installer, you can follow any of the following methods: @@ -136,7 +143,7 @@ To install UnoPim using our GUI installer, you can follow any of the following m 4. Run the following command: ```sh - composer create + composer install ``` 5. Configure your HTTP server to point to the `public/` directory of the project. @@ -150,7 +157,7 @@ To install UnoPim using our GUI installer, you can follow any of the following m ::: warning Important Prerequisites Make sure your system meets these requirements: - Composer is installed on your system -- PHP >= 8.2 +- PHP >= 8.4.1 - Required PHP extensions are enabled - Proper directory permissions are set ::: @@ -185,8 +192,8 @@ Follow these steps to install UnoPim on macOS: brew install composer ``` -5. **Install PostgreSql**: - To install PostgreSql, run the following command: +5. **Install PostgreSQL**: + To install PostgreSQL, run the following command: ```sh brew install postgresql@16 ``` @@ -201,11 +208,11 @@ Follow these steps to install UnoPim on macOS: ```sh composer create-project unopim/unopim ``` - - Chnage directory to project root directory + - Change directory to the project root: ```sh cd unopim ``` -3. **Configure Environment(optional)** : +3. **Configure the environment (optional)**: - Copy the `.env.example` file to `.env` ```sh cp .env.example .env diff --git a/src/3.0/introduction/installation.md b/src/3.0/introduction/installation.md index fe6c4980..cbff2d73 100644 --- a/src/3.0/introduction/installation.md +++ b/src/3.0/introduction/installation.md @@ -11,7 +11,7 @@ To install UnoPim using Composer, follow these steps: - If you have downloaded the zip file from the Git repository, extract the files into your desired directory and run the following command to set up the project: ```sh - composer create-project + composer install ``` - Otherwise, to directly install UnoPim, run the following command in your terminal: @@ -40,30 +40,37 @@ To install UnoPim using Composer, follow these steps: - Follow the prompts during the installation process to provide the following details: ``` - - Please Enter the APP URL : - - Please Enter the Application Name : - - Please select the default locale : - - Please enter the default currency : - - Please choose the Allowed Locales for your channels : - - Please choose the Allowed Currencies for your channels : - - Please select the Database Connection : - - Please enter the Database Host : - - Please enter the Database Port Number : - - Please enter the Database Name : - - Please enter the Database Prefix : - - Please enter the Database Username : - - Please enter the Database Password : + - Please provide the name of the application + - Please provide the application URL + - Please select the default application locale + - Please select the default currency + - Please choose the allowed locales for your channels + - Please choose the allowed currencies for your channels + - Please select the database connection + - Please enter the database host + - Please enter the database port + - Please enter the database name + - Please enter the database prefix + - Please enter your database username + - Please enter your database password + - Do you want to enable Elasticsearch? + (if yes: connection, host or Cloud ID, user, password or API key, index prefix) + - Select optional packages to install - Do you want sample products? [no] [0] yes [1] no ``` - You will then be asked to create your admin credentials: ``` - - Enter the Name of Admin User : - - Enter the Email address of the Admin User : - - Configure the Password for admin user : + - Set the Name for Administrator + - Provide Email of Administrator + - Input a Password for Administrator ``` + ::: tip Command options + `unopim:install` accepts `--skip-env-check`, `--skip-admin-creation`, `--with-demo-data`, and `--with-packages=` (comma-separated: `dam`, `shopify`, `bagisto`). + ::: + - After the installation completes, build the Elasticsearch indexes: ```sh diff --git a/src/3.0/introduction/queue-scheduler-setup.md b/src/3.0/introduction/queue-scheduler-setup.md index b2d3a231..a02a6ee4 100644 --- a/src/3.0/introduction/queue-scheduler-setup.md +++ b/src/3.0/introduction/queue-scheduler-setup.md @@ -42,10 +42,9 @@ The database driver stores jobs in a database table. It requires no additional i QUEUE_CONNECTION=database ``` -Run the migration to create the jobs table (if not already present): +UnoPim ships the `jobs`, `job_batches`, and `failed_jobs` migrations, so switching driver needs no extra table — just run any pending migrations: ```bash -php artisan queue:table php artisan migrate ``` @@ -92,7 +91,7 @@ sudo nano /etc/supervisor/conf.d/unopim-worker.conf ```ini [program:unopim-worker] process_name=%(program_name)s_%(process_num)02d -command=php /var/www/unopim/artisan queue:work redis --queue=system,completeness,default --tries=3 --timeout=90 --max-jobs=1000 --max-time=3600 +command=php /var/www/unopim/artisan queue:work redis --queue=system,completeness,publication,webhooks,default --tries=3 --timeout=90 --max-jobs=1000 --max-time=3600 autostart=true autorestart=true stopasgroup=true @@ -113,7 +112,7 @@ sudo nano /etc/supervisord.d/unopim-worker.ini ```ini [program:unopim-worker] process_name=%(program_name)s_%(process_num)02d -command=php /var/www/unopim/artisan queue:work redis --queue=system,completeness,default --tries=3 --timeout=90 --max-jobs=1000 --max-time=3600 +command=php /var/www/unopim/artisan queue:work redis --queue=system,completeness,publication,webhooks,default --tries=3 --timeout=90 --max-jobs=1000 --max-time=3600 autostart=true autorestart=true stopasgroup=true @@ -129,7 +128,7 @@ stopwaitsecs=3600 | Option | Value | Description | |--------|-------|-------------| -| `--queue` | `system,completeness,default` | Queue names in priority order | +| `--queue` | `system,completeness,publication,webhooks,default` | Queue names in priority order. `publication` carries passport publishing and `webhooks` carries webhook deliveries — omit either and that work queues up unprocessed. | | `--tries` | `3` | Maximum number of attempts before a job is marked as failed | | `--timeout` | `90` | Maximum seconds a job can run before being killed | | `--max-jobs` | `1000` | Restart the worker after processing 1000 jobs (prevents memory leaks) | @@ -218,7 +217,7 @@ php artisan schedule:list ## Monitoring Failed Jobs -When a queued job fails after exhausting all retry attempts, it is stored in the `wk_failed_jobs` table. +When a queued job fails after exhausting all retry attempts, it is stored in the `failed_jobs` table (prefixed, if you set `DB_PREFIX`). ### List Failed Jobs @@ -255,6 +254,8 @@ Check how many jobs are pending in each queue: # With Redis driver redis-cli LLEN queues:system redis-cli LLEN queues:completeness +redis-cli LLEN queues:publication +redis-cli LLEN queues:webhooks redis-cli LLEN queues:default ``` @@ -297,7 +298,7 @@ redis-cli LLEN queues:default Reduce the `--max-jobs` value or add `--memory=128` to limit memory usage per worker: ```ini -command=php /var/www/unopim/artisan queue:work redis --queue=system,completeness,default --tries=3 --timeout=90 --max-jobs=500 --max-time=3600 --memory=128 +command=php /var/www/unopim/artisan queue:work redis --queue=system,completeness,publication,webhooks,default --tries=3 --timeout=90 --max-jobs=500 --max-time=3600 --memory=128 ``` ### Scheduler not running diff --git a/src/3.0/introduction/requirements.md b/src/3.0/introduction/requirements.md index 9cf0e9b5..9ed39ae6 100644 --- a/src/3.0/introduction/requirements.md +++ b/src/3.0/introduction/requirements.md @@ -193,18 +193,24 @@ The `gd` extension must be compiled with JPEG, PNG, and WebP support to avoid is Open your **`php.ini`** file and modify the following settings. -- **memory_limit**: Set the **`memory_limit`** directive to **`4G`** or higher to ensure sufficient memory allocation for the application. +- **memory_limit**: `512M` is enough for serving the admin panel — it is what the shipped Docker image uses. Raise it for the CLI (`/etc/php/8.4/cli/php.ini`), where imports, exports, and reindexing run: `2G` or more on large catalogs. -- **max_execution_time**: Adjust the **`max_execution_time`** directive to **`360`** or higher. This value determines the maximum time (in seconds) a script is allowed to run. Increasing this value ensures that longer operations, such as import/export processes, can be completed successfully. +- **max_execution_time**: `120` seconds covers admin requests. Long-running work belongs on the queue rather than in a web request, so raise this only if you deliberately run imports through the browser. - **date.timezone**: Set the **`date.timezone`** directive to your specific timezone. For example, **`Asia/Kolkata`**. This ensures that date and time-related functions work accurately based on the specified timezone. ```ini -memory_limit = 4G -max_execution_time = 360 -date.timezone = Asia/Kolkata <- Change this to your own timezone. +memory_limit = 512M ; 2G or more for the CLI php.ini +max_execution_time = 120 +upload_max_filesize = 200M +post_max_size = 200M +date.timezone = Asia/Kolkata ; change this to your own timezone ``` +::: tip Keep the two limits in step +`upload_max_filesize` and `post_max_size` must be at least as large as the web server's own limit (`client_max_body_size` on Nginx, `LimitRequestBody` on Apache), or large media and import files are rejected by PHP after the web server has already accepted them. +::: + ::: tip Remember to restart your web server Whenever you make changes to the PHP configuration file, be sure to restart Apache or NGINX to apply the modifications. ::: @@ -213,10 +219,14 @@ Whenever you make changes to the PHP configuration file, be sure to restart Apac ### Redis -Redis is recommended for cache, session storage, and queue processing. UnoPim's `.env.docker` defaults to Redis on all three. +Redis is recommended for cache, session storage, and queue processing. UnoPim's `.env.docker` uses Redis for the cache and the queue; sessions keep Laravel's default driver unless you set one. - **Version**: Redis 7.x (or newer) -- **Use cases**: queue driver (`QUEUE_CONNECTION=redis`), cache (`CACHE_DRIVER=redis`), sessions (`SESSION_DRIVER=redis`) +- **Use cases**: queue driver (`QUEUE_CONNECTION=redis`), cache (`CACHE_STORE=redis`), sessions (`SESSION_DRIVER=redis`) + +::: warning `CACHE_DRIVER` no longer exists +Laravel renamed the variable to `CACHE_STORE`. A leftover `CACHE_DRIVER` line in your `.env` is ignored, and the cache silently falls back to the default store. +::: The `database` driver is supported as a fallback but is slower under load. The `sync` driver should be used for development only. @@ -241,7 +251,7 @@ UnoPim supports the following database servers: - **MySQL**: Version 8.0.32 or higher is recommended for optimal performance and compatibility. -- **MariaDB**: Version 10.3 or higher is recommended for optimal performance and compatibility. +- **MariaDB**: Version 10.6 or higher. MariaDB is not covered by UnoPim's CI matrix, which tests MySQL 8 and PostgreSQL 16 — prefer one of those for production. - **PostgreSQL**: Version 16 is recommended, fully supported, and CI-tested. New Docker installations use PostgreSQL by default as of UnoPim v3.0. diff --git a/src/3.0/introduction/web-server-configuration.md b/src/3.0/introduction/web-server-configuration.md index baf112da..85c6c13f 100644 --- a/src/3.0/introduction/web-server-configuration.md +++ b/src/3.0/introduction/web-server-configuration.md @@ -61,12 +61,30 @@ server { gzip_comp_level 5; gzip_proxied any; + # ────────────────────────────────────────────── + # Dynamic image cache — /cache/{template}/{filename} is rendered by + # PHP, so it must reach the front controller. This block has to come + # BEFORE the static-extension rule below, which would 404 it. + # ────────────────────────────────────────────── + location ^~ /cache/ { + try_files $uri /index.php?$query_string; + } + + # ────────────────────────────────────────────── + # Passport QR carrier — /p/{uuid}/carrier.svg is generated on the fly. + # Same reason: it must precede the static .svg rule. + # ────────────────────────────────────────────── + location ~ ^/p/[^/]+/carrier\.svg$ { + try_files $uri /index.php?$query_string; + } + # ────────────────────────────────────────────── # Static file caching (30 days) # ────────────────────────────────────────────── - location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ { + location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|webp|map|woff|woff2|ttf|eot)$ { expires 30d; add_header Cache-Control "public, immutable"; + access_log off; try_files $uri =404; } @@ -80,39 +98,58 @@ server { # ────────────────────────────────────────────── # PHP processing via PHP-FPM # ────────────────────────────────────────────── - location ~ \.php$ { - try_files $uri =404; - fastcgi_split_path_info ^(.+\.php)(/.+)$; + location ~ ^/index\.php(/|$) { + fastcgi_split_path_info ^(.+\.php)(/.*)$; # Ubuntu/Debian socket path: fastcgi_pass unix:/run/php/php8.4-fpm.sock; # CentOS/RHEL socket path (uncomment if needed): # fastcgi_pass unix:/run/php-fpm/www.sock; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + fastcgi_param DOCUMENT_ROOT $realpath_root; + + # Prevent the httpoxy vulnerability + fastcgi_param HTTP_PROXY ""; + + # Forward client info so url(), secure() and trusted proxies work + fastcgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for; + fastcgi_param HTTP_X_FORWARDED_PROTO $scheme; + fastcgi_param HTTP_X_REAL_IP $remote_addr; # Timeout for long-running operations (imports, exports) fastcgi_read_timeout 600; + fastcgi_send_timeout 600; # Buffer settings for large responses - fastcgi_buffers 16 16k; - fastcgi_buffer_size 32k; + fastcgi_buffers 32 32k; + fastcgi_buffer_size 128k; + fastcgi_busy_buffers_size 256k; + + internal; } # ────────────────────────────────────────────── - # Security: Deny access to hidden files (.env, .git, etc.) + # Security: Never serve executable or active content from uploads # ────────────────────────────────────────────── - location ~ /\. { - deny all; + location ~* ^/storage/.*\.(php[0-9]?|phtml|pht|phar|html?|shtml|xhtml)$ { + return 404; + } + + # ────────────────────────────────────────────── + # Security: only index.php may execute + # ────────────────────────────────────────────── + location ~ \.php$ { + return 404; } # ────────────────────────────────────────────── - # Security: Deny PHP execution in writable directories + # Security: Deny access to hidden files (.env, .git, etc.) # ────────────────────────────────────────────── - location ~* ^/(storage|bootstrap/cache)/.*\.php$ { + location ~ /\. { deny all; + access_log off; } # ────────────────────────────────────────────── @@ -199,6 +236,24 @@ sudo nano /etc/apache2/sites-available/unopim.conf AddOutputFilterByType DEFLATE text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml + # Dynamic routes that must not be treated as static files: + # /cache/{template}/{filename} (image cache) and + # /p/{uuid}/carrier.svg (passport QR carrier) are rendered by PHP. + # Laravel's shipped public/.htaccess already routes anything that is + # not an existing file to index.php, so keep AllowOverride All and do + # not add expiry rules that bypass the front controller for them. + + + ExpiresActive Off + + Header unset Cache-Control + + + # Never serve executable or active content from uploads + + Require all denied + + # Static file caching ExpiresActive On diff --git a/src/3.0/packages/add-menu-in-admin.md b/src/3.0/packages/add-menu-in-admin.md index 9cf38ae3..ae0bf2d5 100644 --- a/src/3.0/packages/add-menu-in-admin.md +++ b/src/3.0/packages/add-menu-in-admin.md @@ -30,7 +30,7 @@ To ensure that the side menu includes the necessary configuration, follow these Open `menu.php` and define your menu items using an array structure. Each item should include: - `key` Unique identifier for the menu item. -- `name` Display name of the menu item. +- `name` Translation key for the label — never a literal string, so the menu follows the admin's locale. - `route` Laravel route name corresponding to the menu item. - `sort` Optional. Sort order for menu items. - `icon` Optional. CSS class for an icon associated with the menu item. @@ -41,7 +41,7 @@ Open `menu.php` and define your menu items using an array structure. Each item s return [ [ 'key' => 'examples', - 'name' => 'Examples', + 'name' => 'example::app.components.layouts.sidebar.menu.examples', 'route' => 'example.menu.index', 'sort' => 2, 'icon' => 'icon-example', diff --git a/src/3.0/packages/blade-components.md b/src/3.0/packages/blade-components.md index 8f03ddac..9d92ab0d 100644 --- a/src/3.0/packages/blade-components.md +++ b/src/3.0/packages/blade-components.md @@ -4,7 +4,7 @@ To ensure optimal user experience in **UnoPim** we have created several separate Blade components for the Admin packages. Now in **`UnoPim`** we have also merged the vue.js code inside the blade component to improve application performance. -Additionally, To learn in detail about blade components, you can visit the Laravel documentation [here](https://laravel.com/docs/10.x/blade#introduction). +Additionally, To learn in detail about blade components, you can visit the Laravel documentation [here](https://laravel.com/docs/13.x/blade#introduction). - Here are the list of Blade component that is available in **`UnoPim`**. @@ -12,6 +12,30 @@ Additionally, To learn in detail about blade components, you can visit the Larav components are reusable Blade components used to build the Admin. +## Component Index + +The admin theme ships **159 Blade components** under `packages/Webkul/Admin/src/Resources/views/components/`. Before writing markup, look for an existing component — hand-rolled HTML drifts from the design system and misses the dark-mode, accessibility, and Vue wiring the components already carry. + +| Family | What it covers | +|---|---| +| `form` | Control groups, labels, controls, errors, the AJAX form wrapper, the unsaved-changes bar | +| `layouts` | Page shell, page headers, edit headers, tabs, side rail, the with-history layout, anonymous layout | +| `datagrid` | The grid itself plus its filters, toolbar, and row templates | +| `table` | Plain tables: `thead`, `tbody`, `tr`, `th`, `td` | +| `modal`, `drawer`, `dropdown`, `accordion`, `tabs` | Overlays and disclosure | +| `media` | `image` and `gallery` upload controls, media cards and fields | +| `tree` | The category tree browser | +| `flat-picker` | Date and datetime pickers | +| `tinymce` | The self-hosted rich-text editor | +| `shimmer` | Loading placeholders, one per component family | +| `flash-group`, `pagination`, `search`, `breadcrumbs`, `badge`, `card` | Common page furniture | +| `history` | The audit-trail panel used by the with-history layout | +| `catalog`, `categories`, `product`, `products`, `associations`, `bulkedit`, `data-transfer`, `settings`, `sso`, `graphs`, `list` | Domain-specific building blocks | + +The sections below document the components you will reach for most often. For anything else, read the component's `@props` block — that is the authoritative prop list. + +--- + ### Accordion UnoPim provides a collapsible accordion UI element, allowing users to toggle the visibility of content sections. It is commonly used for organizing and presenting information in a compact and intuitive manner. @@ -430,7 +454,7 @@ Let's assume you want to use the **`tagging`** component. You can call it like t - Tags + @lang('example::app.admin.form.tags') @php // Example data for existing tags @@ -577,7 +601,7 @@ Let's assume you want to use the **`flat-picker`** component. You can call it li The `datagrid` component in UnoPim applications provides a flexible and customizable data grid interface for displaying tabular data. It includes features such as `sorting`, `filtering`, `pagination`, and `mass actions` to manage data efficiently. -You can customize the appearance of the `DataGrid` by referring to the [DataGrid Customization](https://devdocs.unopim.com/2.x/packages/datagrid.html#datagrid-customization) documentation. +You can customize the appearance of the `DataGrid` by referring to the [DataGrid](./datagrid) documentation. Let's assume you want to use the **`datagrid`** component. You can call it like this. @@ -632,26 +656,6 @@ Let's assume you want to use the **`shimmer`** You can call it like this. ``` -### Quantity Changer - -The Quantity Changer component, provides a simple interface for users to increase or decrease a quantity value. - -| Props | Type | Default Value | Description | -| -------------- | ------- | ------------- | --------------------------------- | -| **`name`** | String | `''` | The name attribute for the hidden input field. | -| **`value`** | Number | `1` | The initial quantity value. | - -Let's assume you want to use the **`Quantity Changer`** component on shop. You can call it like this. - -```html - - -``` - ### Table The Table component provides a structured way to display tabular data in UnoPim. You can customize the appearance of the table elements using CSS. Below are some common customization options: @@ -787,33 +791,35 @@ Let's assume you want to use the **`tree`** component, You can call it like this ### Media(Image/Video) -The Media component in UnoPim provides a user interface for managing and displaying images/videos, allowing users to upload, edit, and delete images.: +The media components render UnoPim's upload controls. Use `media.image` for a single file and `media.gallery` for a gallery attribute, which accepts images and videos alike. + +**`media.gallery`** -| Props | Type | Default Value | Description | -|---------------------|-------------|---------------|------------------------------------------------------------------| -| **`name`** | `String` | | The name of the input field. | -| **`allow-multiple`** | `Boolean` | `false` | Whether to allow uploading multiple images. | -| **`show-placeholders`** | `Boolean` | `true` | Whether to show placeholder images when no images are uploaded. | -| **`uploaded-images`** | `Array` | `[]` | Array of uploaded images. | -| **`uploaded-videos`** | `Array` | `[]` | Array of uploaded videos. | -| **`width`** | `String` | `'100%'` | Width of the image container. | -| **`height`** | `String` | `'auto'` | Height of the image container. | +| Props | Type | Default Value | Description | +|--------------------------|-----------|----------------------------|----------------------------------------------------------------| +| **`name`** | `String` | `'images'` | The name of the input field. | +| **`allow-multiple`** | `Boolean` | `false` | Whether to allow uploading multiple files. | +| **`show-placeholders`** | `Boolean` | `false` | Whether to show placeholder tiles when nothing is uploaded. | +| **`uploaded-images`** | `Array` | `[]` | Already-stored files. | +| **`width`** / **`height`** | `String` | `'120px'` | Size of each tile. | +| **`accepted-types`** | `Array` | `['image/*', 'video/*']` | MIME types the picker accepts. | +| **`accepted-extensions`**| `Array` | `[]` | Restrict to specific extensions. | +| **`instructions`** | `String` | `''` | Helper text shown under the field. | -Let's assume you want to use the **`Image/Video`** component, You can call it like this. +**`media.image`** takes the same `name`, `uploaded-images`, `width`, `height`, and `instructions`, plus `show-suggestions`, `show-upload-hint`, `object-fit`, `responsive`, `has-context`, and `full-preview`. ```html - - + - - + ``` diff --git a/src/3.0/packages/bundling-assets.md b/src/3.0/packages/bundling-assets.md index ecaec0f6..05cca2c7 100644 --- a/src/3.0/packages/bundling-assets.md +++ b/src/3.0/packages/bundling-assets.md @@ -8,7 +8,7 @@ Assets in web development refer to files such as stylesheets, scripts, and image - **JavaScript**: JavaScript (JS) adds interactivity and dynamic behavior to web pages, enabling features like form validation, animations, and AJAX requests. - **Images**: Images enhance visual content, including logos, illustrations, and photographs, making web pages more engaging and informative. -To learn in detail about Bundling Asset, you can visit the Laravel documentation [here](https://laravel.com/docs/10.x/frontend#bundling-assets). +To learn in detail about Bundling Asset, you can visit the Laravel documentation [here](https://laravel.com/docs/13.x/frontend#bundling-assets). ## Directory Structure @@ -81,18 +81,19 @@ Copy and paste the following code into your `package.json` file: "build": "vite build" }, "devDependencies": { - "autoprefixer": "^10.4.14", - "axios": "^1.1.2", - "laravel-vite-plugin": "^0.7.2", + "autoprefixer": "^10.4.16", + "axios": "^1.6.4", + "laravel-vite-plugin": "^1.2.0", "postcss": "^8.4.23", "tailwindcss": "^3.3.2", - "vite": "^4.0.0", - "vue": "^3.2.47" + "vite": "^6.3.0", + "vue": "^3.5.13" }, "dependencies": { "@vee-validate/i18n": "^4.9.1", "@vee-validate/rules": "^4.9.1", - "mitt": "^3.0.0", + "@vitejs/plugin-vue": "^5.0.0", + "mitt": "^3.0.1", "vee-validate": "^4.9.1", "vue-flatpickr": "^2.3.0" } @@ -121,7 +122,8 @@ The `package.json` file includes the following: - **Dependencies:** These are essential packages required for the project to function, including: - `@vee-validate/i18n` Internationalization for VeeValidate. - `@vee-validate/rules` Validation rules for VeeValidate. - - `mitt` A tiny event emitter. + - `@vitejs/plugin-vue` Compiles single-file Vue components; required if your package ships any `.vue` file. + - `mitt` A tiny event emitter — UnoPim's admin uses it for the `$emitter` event bus. - `vee-validate` Form validation for Vue.js. - `vue-flatpickr` A Vue component for Flatpickr date picker. @@ -131,6 +133,7 @@ Copy and paste the following code into your `vite.config.js` file: ```javascript import { defineConfig, loadEnv } from "vite"; +import vue from "@vitejs/plugin-vue"; import laravel from "laravel-vite-plugin"; import path from "path"; @@ -152,6 +155,8 @@ export default defineConfig(({ mode }) => { }, plugins: [ + vue(), + laravel({ hotFile: "../../../public/example-vite.hot", publicDirectory: "../../../public", diff --git a/src/3.0/packages/configurable-associations.md b/src/3.0/packages/configurable-associations.md index 1e6c5e1f..342ee5f3 100644 --- a/src/3.0/packages/configurable-associations.md +++ b/src/3.0/packages/configurable-associations.md @@ -38,7 +38,7 @@ A field with `value_per_locale = 1` reads and writes the `locale_specific. 'example', - 'name' => 'example', + 'name' => 'example::app.acl.example', 'route' => 'example.admin.index', 'sort' => 2 ] ]; ``` -In the above code, we have defined an array for each menu item with the parameters (key, name, route, and sort). You need to define the menus you want to include in the ACL here. +Each array element defines one permission with a `key`, a `name`, the `route` it guards, and a `sort` order. The `name` must be a translation key, not a literal string, so the permission label follows the admin's locale. Keys are flat and dot-separated (`example`, `example.create`, `example.edit`, `example.delete`) — there is no nested `children` array. ## Merge ACL Configuration @@ -63,7 +63,7 @@ Inside the `register` method of your service provider, use the mergeConfigFrom m use Illuminate\Support\ServiceProvider; - class StripeServiceProvider extends ServiceProvider + class ExampleServiceProvider extends ServiceProvider { /** * Register services. @@ -87,10 +87,10 @@ This will merge the ACL configuration with the existing configuration. ### Clear Configuration Cache -After making changes, clear the configuration cache to apply the latest ACL configuration: +After making changes, clear the cached config so the new ACL entries are picked up: ```sh -php artisan optimize +php artisan optimize:clear ``` ### Verify in Admin Panel diff --git a/src/3.0/packages/create-migrations.md b/src/3.0/packages/create-migrations.md index 067ab057..4706de0c 100644 --- a/src/3.0/packages/create-migrations.md +++ b/src/3.0/packages/create-migrations.md @@ -6,7 +6,7 @@ Migrations are like version control for your database, allowing your team to def UnoPim leverages the Laravel Schema facade to offer database-agnostic support for creating and manipulating tables across various database systems supported by Laravel. Migrations in UnoPim utilize this powerful feature to manage database schema changes efficiently. -To understand Migrations in detail, you can visit the Laravel documentation [here](https://laravel.com/docs/10.x/migrations). +To understand Migrations in detail, you can visit the Laravel documentation [here](https://laravel.com/docs/13.x/migrations). Let's create a new migration file for your application. We will assume that the package name is "**Example**". Follow these steps: diff --git a/src/3.0/packages/create-models.md b/src/3.0/packages/create-models.md index 76185cd3..0b5cee28 100644 --- a/src/3.0/packages/create-models.md +++ b/src/3.0/packages/create-models.md @@ -3,7 +3,7 @@ ## Introduction Laravel includes Eloquent, an object-relational mapper (ORM) that makes it enjoyable to interact with your database. When using Eloquent, each database table has a corresponding "Model" that is used to interact with that table. In addition to retrieving records from the database table, Eloquent models allow you to insert, update, and delete records from the table as well. -To understand Models in detail, you can visit the Laravel documentation [here](https://laravel.com/docs/10.x/eloquent). +To understand Models in detail, you can visit the Laravel documentation [here](https://laravel.com/docs/13.x/eloquent). We are using the [konekt/concord](https://packagist.org/packages/konekt/concord) package, which is an extension of Laravel. It helps in building modular Laravel applications. @@ -17,7 +17,7 @@ Before creating the model class, it's essential to create two additional compone Laravel's Contracts are a set of interfaces that define the core services provided by the framework. For example, the **`Illuminate\Contracts\Queue\Queue`** contract defines the methods needed for queueing jobs, while the **`Illuminate\Contracts\Mail\Mailer`** contract defines the methods needed for sending an email. -Each contract has a corresponding implementation provided by the framework. For example, Laravel provides a queue implementation with various drivers and a mailer implementation powered by SwiftMailer. +Each contract has a corresponding implementation provided by the framework. For example, Laravel provides a queue implementation with various drivers and a mailer implementation powered by Symfony Mailer. All Laravel contracts are stored in their own GitHub repository. This provides a quick reference for all available contracts and a single, decoupled package that can be used by package developers. @@ -147,7 +147,7 @@ Copy the following code into the **`Example.php`** file. } ``` -The `Example` model represents a example example in the application. It implements the `ExampleContract` and is part of the `Webkul\Example\Models` namespace. +The `Example` model represents an example record in the application. It implements the `ExampleContract` and is part of the `Webkul\Example\Models` namespace. `public function author(): BelongsTo` This method defines a `BelongsTo` relationship between the Example model and the Admin model. diff --git a/src/3.0/packages/data-transfer.md b/src/3.0/packages/data-transfer.md index 7daa4130..ba3bf661 100644 --- a/src/3.0/packages/data-transfer.md +++ b/src/3.0/packages/data-transfer.md @@ -2,7 +2,7 @@ ## Introduction -Creating custom data import and export functionalities in UnoPim allows seamless bulk data management directly from the admin panel under the `Settings Menu`. This feature is essential for efficiently handling large datasets within your application. +Creating custom data import and export functionalities in UnoPim allows seamless bulk data management directly from the admin panel under **Data Transfer**, its own top-level sidebar section since v3.0 (it previously lived under Settings). This feature is essential for efficiently handling large datasets within your application. ## Import @@ -14,9 +14,9 @@ The feature works differently for each system and has a vast variety of use case Exporting data to save information in files is a common practice for data management, analysis, and sharing. This involves transferring data from a source system into a file format that is suitable for storage, future use, or sharing with others. -## Import/Export Tracker UI (v2.0.0) +## Import/Export Tracker UI -UnoPim v2.0.0 introduced a real-time **Import/Export Tracker** in the admin panel that provides step-by-step pipeline visualization for running jobs. The tracker displays each stage of the import or export process, including validation, processing, and indexing, with live progress indicators. +UnoPim provides a real-time **Import/Export Tracker** in the admin panel that provides step-by-step pipeline visualization for running jobs. The tracker displays each stage of the import or export process, including validation, processing, and indexing, with live progress indicators. ### Key Features @@ -24,7 +24,7 @@ UnoPim v2.0.0 introduced a real-time **Import/Export Tracker** in the admin pane - **Job-specific logging** that captures detailed logs for each import/export run, making it easier to diagnose issues. - **Pause, Resume, and Cancel controls** available directly in the tracker UI, allowing administrators to manage running jobs without terminal access. -## File Upload Enhancements (v2.0.0) +## File Upload Enhancements ### Drag-and-Drop File Upload @@ -34,7 +34,7 @@ Import jobs now support **drag-and-drop file upload** for CSV, XLSX, and XLS fil A dedicated **ZIP image upload modal** with drag-and-drop support is available for importing product images in bulk. Users can upload a ZIP archive containing product images, which are automatically extracted and associated with the corresponding products. -## Optimized Export Pipeline (v2.0.0) +## Optimized Export Pipeline The export pipeline has been significantly optimized for better performance with large datasets: @@ -55,7 +55,7 @@ protected $timeout = 1800; // 30-minute timeout per attempt Tuning these values helps prevent silent failures on long-running exports and ensures jobs are retried automatically when transient errors occur. -## Optimized Import Pipeline (v2.0.0) +## Optimized Import Pipeline The import pipeline includes several performance and reliability improvements: @@ -64,9 +64,19 @@ The import pipeline includes several performance and reliability improvements: - **Batch state tracking** that persists progress per batch, enabling accurate resume after pause or failure. - **Configurable batch and chunk sizes** allowing administrators to tune import performance based on server resources and dataset characteristics. -## Translatable Tracker UI (v2.0.0) +## Translatable Tracker UI All tracker UI elements now use **translation strings** instead of hardcoded text. Labels such as "Importing", "Exporting", step names, and status messages are fully translatable, making it straightforward to localize the entire import/export experience. - Labels, button text, and progress messages are resolved through Laravel's translation helpers (`trans()` / `__()`). -- Custom tracker labels can be overridden by publishing or editing the corresponding language files in your package's `Resources/lang/{locale}/` directory. \ No newline at end of file +- Custom tracker labels can be overridden by publishing or editing the corresponding language files in your package's `Resources/lang/{locale}/` directory. + +## What v3.0 Added + +- **More entities.** Import and export jobs now cover attributes, attribute groups, attribute families, attribute options, category fields, configurable associations, locales, channels, currencies, roles, and users — each with localized values and a downloadable sample file. +- **Richer product export filters.** Channels, locales, currencies, attributes, families, status, completeness, last-N-days / since-last-export / between-date conditions, categories, identifiers, and attribute-value conditions. +- **Quick export and async mass actions.** Large selections are queued instead of blocking the request. +- **Keyset pagination** replaces offset pagination in large exports, so cost no longer grows with depth. +- **Moved URLs.** `settings/data-transfer/*` is now `data-transfer/*`, and the tracker moved from `tracker/*` to `data-transfer/job-tracker/*`. Update any bookmarks or extension links — see the [Upgrade Guide](../prologue/upgrade-guide#changed-admin-urls). + +Building your own import or export profile? See [Export Profile](../plugins/create-export-profile) and [Import Profile](../plugins/create-import-profile). diff --git a/src/3.0/packages/datagrid.md b/src/3.0/packages/datagrid.md index 88b92366..3878cc60 100644 --- a/src/3.0/packages/datagrid.md +++ b/src/3.0/packages/datagrid.md @@ -23,7 +23,7 @@ The DataGrid in UnoPim has several global properties that enhance its functional The **`DataGrid`** abstract class is created in the **`Webkul\DataGrid`** package. In the abstract class, a list of properties and methods are declared. To create your own DataGrid, you need to extend the **`Webkul\DataGrid\DataGrid`** abstract class. -In **`Webkul\DataGrid\DataGrid\DataGrid.php`** abstract class, two abstract methods are declared **`prepareQueryBuilder()`** and **`prepareColumns()`**. You can prepare your grid by defining these two methods. +In the **`Webkul\DataGrid\DataGrid`** abstract class, two abstract methods are declared **`prepareQueryBuilder()`** and **`prepareColumns()`**. You can prepare your grid by defining these two methods. - **`prepareQueryBuilder()`**: In this method, records are retrieved through queries applicable to the database and stored in a collection. When records are retrieved, the **`setQueryBuilder()`** method is called. @@ -77,17 +77,21 @@ In **`Webkul\DataGrid\DataGrid\DataGrid.php`** abstract class, two abstract meth ```php public function prepareActions() { - $this->addAction([ - 'icon' => 'icon-edit', - 'title' => trans('example::app.admin.datagrid.edit'), - 'method' => 'GET', - 'url' => function ($row) { - return route('admin.example.edit', $row->id); - }, - ]); + if (bouncer()->hasPermission('example.edit')) { + $this->addAction([ + 'icon' => 'icon-edit', + 'title' => trans('example::app.admin.datagrid.edit'), + 'method' => 'GET', + 'url' => function ($row) { + return route('admin.example.edit', $row->id); + }, + ]); + } } ``` + Gate every action behind a `bouncer()->hasPermission()` check, as the core grids do — a row action the user may not perform should not be rendered at all. + ## Making DataGrids 1. Create a folder called **`DataGrids`** inside the **`src`** folder of your package. Within the **`DataGrids`** folder, create a file name **`ExampleDataGrid.php`**. @@ -176,7 +180,7 @@ class ExampleDataGrid extends DataGrid * * @var string */ - protected $primaryColumn = 'order_id'; + protected $primaryColumn = 'id'; /** * Prepare query builder. @@ -266,7 +270,7 @@ class ExampleDataGrid extends DataGrid 'title' => trans('example::app.admin.datagrid.edit'), 'method' => 'GET', 'url' => function ($row) { - return route('aadmin.example.edit', $row->id); + return route('admin.example.edit', $row->id); }, ]); @@ -289,7 +293,7 @@ class ExampleDataGrid extends DataGrid { $this->addMassAction([ 'title' => trans('example::app.admin.datagrid.mass-update'), - 'url' => oute('admin.example.mass_update'), + 'url' => route('admin.example.mass_update'), 'method' => 'POST', 'options' => [ [ diff --git a/src/3.0/packages/history.md b/src/3.0/packages/history.md index 9eb610e7..9b1a15b4 100644 --- a/src/3.0/packages/history.md +++ b/src/3.0/packages/history.md @@ -71,14 +71,20 @@ To control which fields should or should not be tracked in the history, you can To display the history of a model, use the following layout in the model's edit page: ```blade - + attributeFamily + + + @lang('example::app.admin.edit.title') + + + {{-- form content --}} ``` -The `entityName` slot defines the history tag to group the related records. +The `entityName` slot carries the history tag that groups the related records, and `history-id` is the record whose trail the History tab loads — it falls back to `request()->id` when omitted. The component also accepts `active-tab`, `general-url`, `history-url`, and `tab-items` for pages with more tabs than General and History. See [Layouts](./layouts#edit-pages-with-history). ## Handling Translatable Fields diff --git a/src/3.0/packages/index.md b/src/3.0/packages/index.md index 65c817d5..0f681ed3 100644 --- a/src/3.0/packages/index.md +++ b/src/3.0/packages/index.md @@ -12,7 +12,8 @@ In UnoPim, various packages are located at **`packages/Webkul/`**. Below is a ba └── src ├── Config │ ├── acl.php - │ └── menu.php + │ ├── menu.php + │ └── system_settings.php ├── Console │ └── Commands ├── Contracts @@ -54,7 +55,9 @@ In UnoPim, various packages are located at **`packages/Webkul/`**. Below is a ba │ └── css │ └── app.css ├── lang - │ └── app.php + │ ├── en_US + │ │ └── app.php + │ └── … # one directory per supported locale └── views └── example ├── create.blade.php diff --git a/src/3.0/packages/layouts.md b/src/3.0/packages/layouts.md index 1bf8a656..44ca8444 100644 --- a/src/3.0/packages/layouts.md +++ b/src/3.0/packages/layouts.md @@ -4,30 +4,83 @@ Layouts in UnoPim are fundamental to structuring your application's views in a consistent and reusable way. They provide a template for rendering HTML across multiple pages, ensuring a unified design and user experience. By defining layouts, you can streamline development, improve maintainability, and enhance the overall aesthetics of your web application. -To learn in detail about Layouts, you can visit the Laravel documentation [here](https://laravel.com/docs/10.x/blade). +To learn in detail about Blade layouts, you can visit the Laravel documentation [here](https://laravel.com/docs/13.x/blade). ## Admin Layout -`` This component serves as the container for your extended admin layout. It encapsulates the entire layout structure, including the title and content. +`` is the container for any admin page. It supplies the sidebar, header, breadcrumbs, dark-mode handling, flash messages, and the global unsaved-changes bar — so an admin view should never emit its own `` document. -To extend the default layout of the UnoPim admin panel, you'll create or modify the `index.blade.php` file located at `packages/Webkul/Example/src/Resources/views/admin/index.blade.php`. Below is a detailed breakdown of how to integrate and customize the layout: +To build a listing page for your package, create `packages/Webkul/Example/src/Resources/views/admin/index.blade.php`: -```html +```blade - @lang('example::app.admin.index.page-title') - - -
-

- - @lang('example::app.admin.index.page-title') -

- -
- -
-
+ + + + + @if (bouncer()->hasPermission('example.create')) + + @lang('example::app.admin.index.create-btn') + + @endif + + + +
``` + +## Page Headers + +`` renders the title row of a listing page. + +| Prop | Type | Default | Description | +|---|---|---|---| +| `title` | String | — | The page title. | +| `description` | String | `null` | Optional subtitle under the title. | +| `breadcrumb` | Boolean | `true` | Whether to render breadcrumbs above the title. | + +It exposes an `actions` slot for buttons on the right-hand side. + +For edit screens, use `` instead — it adds a back link and participates in the sticky-header and save-bar behaviour: + +| Prop | Type | Default | Description | +|---|---|---|---| +| `title` | String | — | The page title. | +| `backUrl` | String | `null` | Where the back link points. | +| `backLabel` | String | Back | Label for the back link. | +| `saveLabel` | String | `null` | Label for the save action; suppressed automatically while viewing history. | +| `form` | String | `null` | Id of the form the save action submits. | +| `sticky` | Boolean | `true` | Keep the header pinned while scrolling. | +| `breadcrumb` | Boolean | `true` | Whether to render breadcrumbs. | + +## Edit Pages with History + +Entities that track history use ``, which renders the edit page and a History tab beside it without any extra wiring: + +```blade + + + @lang('example::app.admin.edit.title') + + + + {{-- your edit-page-header --}} + + + {{-- form content --}} + +``` + +`entity-name` is the history entity key your model registers, and `history-id` is the record whose audit trail the tab loads. See [History Tracking](./history) for making a model auditable. + +## Anonymous Layout + +`` is the layout for pages rendered outside the panel — login, password reset, and the installer. It carries the same theme handling but no sidebar or header. diff --git a/src/3.0/packages/localization.md b/src/3.0/packages/localization.md index 76b53b9c..c4a49bf9 100644 --- a/src/3.0/packages/localization.md +++ b/src/3.0/packages/localization.md @@ -269,6 +269,18 @@ php artisan unopim:translations:check --package=Admin php artisan unopim:translations:check --locale=fr_FR --package=Admin ``` +### Coverage Gaps in Code + +Two checks compare the lang files against the source tree rather than against `en_US`: + +```bash +# Keys referenced in code (trans/__/@lang) with no entry in the lang files +php artisan unopim:translations:check --missing-in-code + +# Lang keys that no source file references any more +php artisan unopim:translations:check --unused +``` + ### Detailed Diagnostics ```bash @@ -369,6 +381,7 @@ app(\Webkul\MagicAI\Repository\MagicAIPlatformRepository::class)->create([ | DeepSeek | `deepseek` | DeepSeek models | | Azure OpenAI | `azure` | Azure-hosted OpenAI | | OpenRouter | `openrouter` | Multi-provider gateway | +| Custom | `custom` | Any OpenAI-compatible `/chat/completions` endpoint | ### Translate Missing Keys @@ -443,6 +456,8 @@ With `--fallback`: | `--empty-values` | — | Detect blank/empty values | | `--sort-check` | — | Verify key ordering matches `en_US` | | `--html-check` | — | Verify HTML tag consistency | +| `--missing-in-code` | — | Keys used in code but absent from lang files | +| `--unused` | — | Lang keys no source file references | ### Full Workflow Example diff --git a/src/3.0/packages/routes.md b/src/3.0/packages/routes.md index 516a7401..a2d0bcdf 100644 --- a/src/3.0/packages/routes.md +++ b/src/3.0/packages/routes.md @@ -6,7 +6,7 @@ Routes in Laravel define the entry points of your application, mapping HTTP requ Routes can be defined to handle various HTTP methods (GET, POST, PUT, DELETE, etc.) and can include parameters and route parameters to capture dynamic values from the URL. Laravel's routing system is powerful and flexible, allowing for easy RESTful routing and middleware application to routes. -For detailed information on Laravel routes, including how to define routes, use route parameters, and apply middleware, refer to the [Laravel Documentation on Routing](https://laravel.com/docs/10.x/routing). +For detailed information on Laravel routes, including how to define routes, use route parameters, and apply middleware, refer to the [Laravel Documentation on Routing](https://laravel.com/docs/13.x/routing). ## Create a New Route @@ -38,7 +38,7 @@ Create `routes.php` for admin-specific routes. Add the following code to this fi use Illuminate\Support\Facades\Route; use Webkul\Example\Http\Controllers\ExampleController; -Route::group(['middleware' => ['web', 'admin'], 'prefix' => config('app.admin_url')], function () { +Route::group(['middleware' => ['admin'], 'prefix' => config('app.admin_url')], function () { /** * Example routes for admin. */ @@ -51,7 +51,11 @@ Route::group(['middleware' => ['web', 'admin'], 'prefix' => config('app.admin_ur #### Explanation -Routes inside `routes.php` are prefixed with the admin URL (`config('app.admin_url')`) and apply the `web` and `admin` middleware groups. Adjust the middleware and URL prefix according to your application's configuration. +Routes inside `routes.php` are prefixed with the admin URL (`config('app.admin_url')`) and apply the `admin` middleware group. + +::: warning Use `['admin']`, not `['web', 'admin']` +The `admin` group already includes the session, CSRF, and cookie middleware that `web` provides. Listing both runs that stack twice, which breaks CSRF token handling on admin forms. Every core route group in UnoPim uses `['admin']` alone. +::: ## Loading Routes diff --git a/src/3.0/packages/store-data-through-repositories.md b/src/3.0/packages/store-data-through-repositories.md index 7e121f23..0d8b69f9 100644 --- a/src/3.0/packages/store-data-through-repositories.md +++ b/src/3.0/packages/store-data-through-repositories.md @@ -23,7 +23,7 @@ Manually setting up repository files involves creating and organizing repository ### Setting Up ExampleRepository in Webkul/Example Package -Start by creating a `Repository` folder within the `Webkul/Example/src/` directory. This folder will house the repository class responsible for handling example-related database operations.Create a file named `ExampleRepository.php`. +Start by creating a `Repositories` folder within the `Webkul/Example/src/` directory. This folder will house the repository class responsible for handling example-related database operations.Create a file named `ExampleRepository.php`. ``` └── packages @@ -31,7 +31,7 @@ Start by creating a `Repository` folder within the `Webkul/Example/src/` directo └── Example └── src ├── ... - └── Repository + └── Repositories └── ExampleRepository.php ``` @@ -41,7 +41,7 @@ Copy the following code into your newly created repository file. ```php exampleRepository->findOrFail($id); Create a new record. ```php -$example = $this->exampleRepository->create(Input::all()); +$example = $this->exampleRepository->create($request->validated()); ``` ### Update @@ -123,7 +123,7 @@ $example = $this->exampleRepository->create(Input::all()); Update an existing record by its ID. ```php -$example = $this->exampleRepository->update(Input::all(), $id); +$example = $this->exampleRepository->update($request->validated(), $id); ``` ### Delete diff --git a/src/3.0/packages/swatch-types.md b/src/3.0/packages/swatch-types.md index 665e6207..87de874f 100644 --- a/src/3.0/packages/swatch-types.md +++ b/src/3.0/packages/swatch-types.md @@ -2,7 +2,7 @@ ## Introduction -UnoPim v2.0.0 introduced **swatch types** for `select` and `multiselect` attributes. Swatches provide a visual representation of attribute options, replacing plain text labels with color blocks, image thumbnails, or styled text chips. +UnoPim supports **swatch types** for `select` and `multiselect` attributes. Swatches provide a visual representation of attribute options, replacing plain text labels with color blocks, image thumbnails, or styled text chips. Swatch values are displayed in: @@ -28,13 +28,13 @@ When no swatch type is set, the attribute option displays its standard translate Swatch data is stored across two existing tables: -### `wk_attributes` Table +### `attributes` Table | Column | Type | Description | | ------------- | ----------------- | ----------- | | `swatch_type` | `string` (nullable) | One of `color`, `image`, `text`, or `null` | -### `wk_attribute_options` Table +### `attribute_options` Table | Column | Type | Description | | -------------- | ----------------- | ----------- | diff --git a/src/3.0/packages/validation.md b/src/3.0/packages/validation.md index 867cdd66..a0d78245 100644 --- a/src/3.0/packages/validation.md +++ b/src/3.0/packages/validation.md @@ -8,185 +8,111 @@ Laravel offers multiple approaches to validate incoming data in your application This method is easy to use and integrates seamlessly with Laravel's request lifecycle. By leveraging Laravel's built-in validation rules and custom validation logic, you can ensure your application handles data validation efficiently and effectively. -For detailed information about validation in Laravel, refer to the [Laravel documentation](https://laravel.com/docs/10.x/validation). +For detailed information about validation in Laravel, refer to the [Laravel documentation](https://laravel.com/docs/13.x/validation). ### Usage -Laravel provides multiple ways to handle validation in your application, ensuring your data meets specified criteria before processing it. Here are the two most common methods: +UnoPim validates every write through a **FormRequest** class. Inline `$request->validate()` calls are not used in core and should not be used in packages: a FormRequest keeps rules out of the controller, gives you an `authorize()` hook, and is reusable across the store and update paths. -### Using the validate Method on Request +### Creating a FormRequest -The simplest and most common way to validate incoming data is to use the `validate` method available on incoming HTTP requests. Here’s an example of how you can use this method to validate data in a controller method: +Put the class in your package's `Http/Requests` folder: ```php -/** - * Store a new example example. - */ -public function store(Request $request) -{ - $validated = $request->validate([ - 'title' => 'required|unique:examples|max:255', - 'body' => 'required', - ]); -} -``` - -In this example, the validate method takes an array of validation rules. If the validation fails, a ValidationException is thrown, and the user is redirected back to the previous page with error messages. - -### Using the Validator Facade + 'required', - 'email' => 'required|email', - 'message' => 'required|max:250', - ]; + return bouncer()->hasPermission('example.create'); + } - $customMessages = [ - 'required' => 'The :attribute field is required.', + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules(): array + { + return [ + 'code' => ['required', 'unique:examples,code', new Code], + 'title' => ['required', 'max:255'], + 'description' => ['nullable', 'string'], + 'status' => ['boolean'], ]; + } - $this->validate($request, $rules, $customMessages); + /** + * Custom messages for the rules above. + * + * @return array + */ + public function messages(): array + { + return [ + 'code.unique' => trans('example::app.validation.code-taken'), + ]; } } ``` -- `Defining Rules` The $rules array contains the validation rules for each field. -- `Custom Messages` The $customMessages array allows you to define custom validation messages. -- `Creating Validator` The Validator::make method creates a validator instance. -- `Handling Failure` If validation fails, the user is redirected back with the validation errors and input data. - -Both methods provide a robust way to ensure data integrity and user input validation in your Laravel application. +Messages must come from translation files — never hardcode the English string, or the error will not follow the admin's locale. -## Validation Using Vue +### Using It in the Controller -### Introduction +Type-hint the request. Laravel resolves it, runs the rules before your method body, and returns a `422` with the error bag when they fail: -VeeValidate is a powerful validation library for Vue.js that provides an extensive set of validation rules out of the box, along with support for custom rules. It is template-based, making it easy to validate HTML5 inputs as well as custom Vue components. VeeValidate also supports localization with 44 languages maintained by the community. - -For detailed information about validation in Vue.js using VeeValidate v4, refer to the [VeeValidate documentation](https://vee-validate.logaretm.com/v4/guide/overview/). +```php +use Webkul\Example\Http\Requests\ExampleRequest; -### Installation +public function store(ExampleRequest $request): JsonResponse +{ + $example = $this->exampleRepository->create($request->validated()); -UnoPim already includes the VeeValidate v4 library, so there is no need to install it separately. + return new JsonResponse([ + 'message' => trans('example::app.examples.create-success'), + ]); +} +``` -### Configuration +`$request->validated()` returns only the keys that passed a rule, which keeps unexpected input out of a mass-assignment call. -UnoPim comes with pre-configured settings for `vee-validate`. You can find the configuration in the following path: `unopim/packages/Webkul/Admin/src/Resources/assets/js/app.js`. - -```js -/** - * This will track all the images and fonts for publishing. - */ -import.meta.glob(["../images/**", "../fonts/**"]); - -/** - * Main vue bundler. - */ -import { createApp } from "vue/dist/vue.esm-bundler"; - -/** - * We are defining all the global rules here and configuring - * all the `vee-validate` settings. - */ -import { configure, defineRule } from "vee-validate"; -import { localize } from "@vee-validate/i18n"; -import en from "@vee-validate/i18n/dist/locale/en.json"; -import * as AllRules from '@vee-validate/rules'; - -/** - * Registration of all global validators. - */ -Object.keys(AllRules).forEach(rule => { - defineRule(rule, AllRules[rule]); -}); +### Custom Rules -/** - * This regular expression allows phone numbers with the following conditions: - * - The phone number can start with an optional "+" sign. - * - After the "+" sign, there should be one or more digits. - * - * This validation is sufficient for global-level phone number validation. If - * someone wants to customize it, they can override this rule. - */ -defineRule("phone", (value) => { - if (!value || !value.length) { - return true; - } +For validation that repeats across requests, write a rule class rather than a closure. UnoPim ships several you can reuse — `Webkul\Core\Rules\Code` for entity codes, and `Webkul\Core\Rules\FileMimeExtensionMatch` for uploads where the extension must match the real MIME type. - if (!/^\+?\d+$/.test(value)) { - return false; - } +## Validation Using Vue - return true; -}); +### Introduction -defineRule("decimal", (value, { decimals = '*', separator = '.' } = {}) => { - if (value === null || value === undefined || value === '') { - return true; - } +VeeValidate is a powerful validation library for Vue.js that provides an extensive set of validation rules out of the box, along with support for custom rules. It is template-based, making it easy to validate HTML5 inputs as well as custom Vue components. VeeValidate also supports localization with 44 languages maintained by the community. - if (Number(decimals) === 0) { - return /^-?\d*$/.test(value); - } +For detailed information about validation in Vue.js using VeeValidate v4, refer to the [VeeValidate documentation](https://vee-validate.logaretm.com/v4/guide/overview/). - const regexPart = decimals === '*' ? '+' : `{1,${decimals}}`; - const regex = new RegExp(`^[-+]?\\d*(\\${separator}\\d${regexPart})?([eE]{1}[-]?\\d+)?$`); +### Installation - return regex.test(value); -}); +UnoPim already includes the VeeValidate v4 library, so there is no need to install it separately. -defineRule("required_if", (value, { condition = true } = {}) => { - if (condition) { - if (value === null || value === undefined || value === '') { - return false; - } - } +### Configuration - return true; -}); +UnoPim ships pre-configured `vee-validate` settings in `packages/Webkul/Admin/src/Resources/assets/js/plugins/vee-validate.js`, registered from `app.js`. The plugin: -defineRule("", () => true); +- registers every rule from `@vee-validate/rules` globally, +- registers UnoPim's own rules — `phone`, `address`, `decimal`, and `required_if`, +- registers the `VForm`, `VField`, and `VErrorMessage` components, +- loads the `@vee-validate/i18n` message catalogue for each supported locale, so validation errors appear in the admin's language, +- and validates on blur, input, and change. -configure({ - /** - * Built-in error messages and custom error messages are available. Multiple - * locales can be added in the same way. - */ - generateMessage: localize({ - en: { - ...en, - messages: { - ...en.messages, - phone: "This {field} must be a valid phone number", - }, - }, - }), - - validateOnBlur: true, - validateOnInput: true, - validateOnChange: true, -}); -``` +Because the rules are global, a Blade control only needs the `rules` attribute — no imports or per-component setup. ### Examples @@ -274,4 +200,18 @@ defineRule( return regex.test(value); } ); -``` \ No newline at end of file +``` + +- `required_if` Makes a field required only when a condition you pass is true — useful for fields that appear conditionally in a form. + +```javascript +defineRule("required_if", (value, { condition = true } = {}) => { + if (condition) { + if (value === null || value === undefined || value === '') { + return false; + } + } + + return true; +}); +``` diff --git a/src/3.0/packages/views.md b/src/3.0/packages/views.md index ddc8b7cd..6c58249e 100644 --- a/src/3.0/packages/views.md +++ b/src/3.0/packages/views.md @@ -6,7 +6,7 @@ Views in Laravel are responsible for separating the application's logic from the By using views, you can create reusable templates and components, making your code more maintainable and easier to understand. Blade templates allow you to use control structures like loops and conditionals, as well as to include other templates, which helps to keep your views organized and modular. -To learn in detail about Views, you can visit the Laravel documentation [here](https://laravel.com/docs/10.x/views). +To learn in detail about Views, you can visit the Laravel documentation [here](https://laravel.com/docs/13.x/views). Here's a basic example of a Blade template: @@ -21,8 +21,8 @@ To organize the views for our example package, we need to set up a specific dire #### Create the `views` Folder - Inside the `Resources` folder, create another folder named `views`. -#### Create the `example` Folders - - Inside the `views` folder, create two folders named `example`. +#### Create the `example` Folder + - Inside the `views` folder, create a folder named `example`. The updated directory structure will look like this: @@ -42,19 +42,20 @@ Below is an example of basic HTML content that you can add to the example `index #### `index.blade.php` in the `example` Folder -```html - - - - Example - - -

Example

-

Welcome to the example section for managing example content.

- - +```blade + + + @lang('example::app.examples.index.title') + + + + +

@lang('example::app.examples.index.description')

+
``` +Admin pages wrap their content in the `x-admin::layouts` component rather than emitting their own `` document — that is what gives the page the sidebar, header, dark-mode handling, and the unsaved-changes bar. Titles and copy come from translation keys, never literal strings. + ## Load Views from Package To make the views in our package accessible, we need to register them in the service provider's `boot` method. This involves updating the `ExampleServiceProvider.php` file to include the view loading logic. Follow the steps below: @@ -110,17 +111,17 @@ In Laravel applications, views are typically rendered from controller methods us ```php 'examples', - 'name' => 'Examples', + 'name' => 'example::app.components.layouts.sidebar.menu.examples', 'route' => 'example.menu.index', 'sort' => 2, 'icon' => 'icon-example', // Optional icon class diff --git a/src/3.0/plugins/create-export-profile.md b/src/3.0/plugins/create-export-profile.md index 2d02785c..9ed22641 100644 --- a/src/3.0/plugins/create-export-profile.md +++ b/src/3.0/plugins/create-export-profile.md @@ -89,9 +89,9 @@ class Exporter extends AbstractExporter To make the exporter available in UnoPim, you need to register it. This involves defining it in a configuration file and loading that configuration within your service provider. -### Step 1: Create `exporter.php` +### Step 1: Create `exporters.php` -In your plugin's `Config` directory, create a new configuration file named `exporter.php`. This file will hold the configuration settings for your exporter. +In your plugin's `Config` directory, create a new configuration file named `exporters.php`. This file will hold the configuration settings for your exporter. Directory structure: @@ -102,12 +102,12 @@ Directory structure: ├── ... └── src └── Config - └── exporter.php + └── exporters.php ``` ### Step 2: Define the Exporter Configuration -In the `exporter.php` file, define your exporter and its settings. Here’s an example configuration for a product exporter: +In the `exporters.php` file, define your exporter and its settings. Here’s an example configuration for a product exporter: ```php [ [ 'name' => 'file_format', - 'title' => 'File Format', + 'title' => 'example::app.exporters.fields.file-format', 'type' => 'select', 'required' => true, 'validation' => 'required', @@ -393,12 +393,12 @@ In your `ExampleServiceProvider`, add the following code to the `register()` met public function register() { $this->mergeConfigFrom( - dirname(__DIR__) . '/Config/exporter.php', 'exporters' + dirname(__DIR__) . '/Config/exporters.php', 'exporters' ); } ``` -This merges the custom `exporter.php` configuration into the core exporter settings in UnoPim. +This merges the custom `exporters.php` configuration into the core exporter settings in UnoPim. ## Step 4: Queue Operations diff --git a/src/3.0/plugins/create-import-profile.md b/src/3.0/plugins/create-import-profile.md index 98cac86c..f15ba9aa 100644 --- a/src/3.0/plugins/create-import-profile.md +++ b/src/3.0/plugins/create-import-profile.md @@ -155,9 +155,9 @@ class Importer extends AbstractImporter Now that the importer is created and the logic is defined, you need to register the importer so that UnoPim recognizes and can use it. -### Step 1: Create `importer.php` +### Step 1: Create `importers.php` -In the `Config` directory of your plugin, create a new configuration file named `importer.php`. This file will contain the configuration for your importers. +In the `Config` directory of your plugin, create a new configuration file named `importers.php`. This file will contain the configuration for your importers. Directory structure: @@ -168,12 +168,12 @@ Directory structure: ├── ... └── src └── Config - └── importer.php + └── importers.php ``` ### Step 2: Define the Importer Configuration -In the `importer.php` file, define the configuration for your importer, specifying the importer class and other important settings like the title and sample file path. +In the `importers.php` file, define the configuration for your importer, specifying the importer class and other important settings like the title and sample file path. ```php mergeConfigFrom( - dirname(__DIR__) . '/Config/importer.php', 'importers' + dirname(__DIR__) . '/Config/importers.php', 'importers' ); } ``` -This ensures that the `importer.php` configuration is merged into the system, allowing UnoPim to recognize the importer. +This ensures that the `importers.php` configuration is merged into the system, allowing UnoPim to recognize the importer. ## Step 4: Queue Operations diff --git a/src/3.0/plugins/create-plugin.md b/src/3.0/plugins/create-plugin.md index 8d689a46..300200ee 100644 --- a/src/3.0/plugins/create-plugin.md +++ b/src/3.0/plugins/create-plugin.md @@ -96,24 +96,20 @@ Add your plugin's namespace to the **`psr-4`** section in the **`composer.json`* } ``` -Register your plugin's service provider in the **`config/app.php`** file located in the root directory of your UnoPim application. Add the following line to the **`providers`** array: +Register your plugin's service provider in the **`bootstrap/providers.php`** file located in the root directory of your UnoPim application. Add your service provider class to the returned array: ```php - ServiceProvider::defaultProviders()->merge([ - // Other service providers - Webkul\Example\Providers\ExampleServiceProvider::class, - ])->toArray(), - - // Other configuration options + // Other service providers + Webkul\Example\Providers\ExampleServiceProvider::class, ]; ``` +::: warning Not `config/app.php` +Since Laravel 11, providers are registered in `bootstrap/providers.php`; the `providers` array in `config/app.php` no longer exists. UnoPim 3.0 runs on Laravel 13, so a provider added to `config/app.php` is simply never loaded. +::: + ### Run the Commands Run the following command to autoload your plugin: diff --git a/src/3.0/plugins/index.md b/src/3.0/plugins/index.md index 1149a3c7..c4f96b9f 100644 --- a/src/3.0/plugins/index.md +++ b/src/3.0/plugins/index.md @@ -13,8 +13,8 @@ packages └── src ├── Config │ ├── acl.php # Access control list configurations - │ ├── exporter.php # configuration the exporter - │ ├── importer.php # configuration the importer + │ ├── exporters.php # exporter configuration + │ ├── importers.php # importer configuration │ └── menu.php # Side menu configuration ├── Console │ └── Commands # Console commands for scheduling imports/exports diff --git a/src/3.0/prologue/contribution-guide.md b/src/3.0/prologue/contribution-guide.md index 9cf86900..ac78467f 100644 --- a/src/3.0/prologue/contribution-guide.md +++ b/src/3.0/prologue/contribution-guide.md @@ -29,7 +29,7 @@ We welcome proposals for new features and enhancements to the existing UnoPim ap Before submitting a pull request, it's important to consider the following points to help you choose the appropriate branch: - **Bug Fixes**: If you're fixing a bug, send the fix to the `master` branch. -- **Critical Bug Fixes**: If you're fixing a critical bug, also port the fix to the latest stable release branch (currently **v3.0.0**) so it can ship in the next patch release. +- **Critical Bug Fixes**: If you're fixing a critical bug, also port the fix to the latest stable release branch (currently **3.0**) so it can ship in the next patch release. - **Feature Requests**: If your request involves a feature with potential breaking changes, send it to the `master` branch, which corresponds to the upcoming release. ## Compiled Assets @@ -78,16 +78,43 @@ php artisan unopim:translations:check This command verifies that every supported locale contains all required keys and reports any missing translations. The same command runs in CI — pull requests with missing translations will fail the build. ::: tip -Treat the translation check as part of the standard pre-commit cycle: write code → run Pint → run translation check → run Pest tests → submit PR. +Treat the translation check as part of the standard pre-commit cycle: write code → run Pint → run the translation check → run Pest → submit the pull request. ::: -## Pint Tests +## Before You Submit -Pint tests are an essential part of ensuring the quality and reliability of code changes in UnoPim. When making changes to the code, ensure that all Pint tests pass before submitting your pull request.Before submitting your changes, run the Pint tests locally to verify that all test cases pass. It is important to confirm that the modifications do not cause any Pint test failures or regressions. +Four checks run in CI. Run them locally first — a pull request that fails any of them cannot be merged. -* To run the Pint tests locally, execute the following command in your terminal: -```php +**Format the code with Pint.** Pint rewrites your files to match the project's style; `--test` reports without changing anything. + +```bash vendor/bin/pint +vendor/bin/pint --test +``` + +**Run the tests with Pest.** Run the whole suite before submitting, or a single file while you work. + +```bash +vendor/bin/pest +vendor/bin/pest --filter=ProductTest +``` + +**Analyse types with Larastan.** Fix the underlying type or logic problem rather than suppressing the error with a baseline entry or an inline annotation. + +```bash +vendor/bin/phpstan analyse --memory-limit=1G +``` + +**Check translations**, as described above: + +```bash +php artisan unopim:translations:check +``` + +If your change touches admin screens, also run the relevant Playwright specs: + +```bash +cd tests/e2e-pw && npx playwright test ``` ## Security Vulnerabilities @@ -96,7 +123,7 @@ If you discover a security vulnerability within UnoPim, please notify us immedia ## Coding Style -UnoPim follows the [PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) coding standard and the [PSR-4](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md) autoloading standard. These standards ensure consistency and readability in the codebase, similar to Laravel. +UnoPim follows the [PSR-12](https://www.php-fig.org/psr/psr-12/) coding standard and the [PSR-4](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md) autoloading standard, applied through Laravel Pint's `laravel` preset. These standards ensure consistency and readability in the codebase, similar to Laravel. In addition to PSR-2 and PSR-4, here are some Laravel and UnoPim-specific coding practices that should be followed: diff --git a/src/3.0/prologue/index.md b/src/3.0/prologue/index.md index 7cf79b28..8b64546e 100644 --- a/src/3.0/prologue/index.md +++ b/src/3.0/prologue/index.md @@ -8,8 +8,17 @@ Built on the reliable [Laravel](https://laravel.com/) framework, [Tailwind CSS]( ## Customization and Data Management -UnoPim offers the flexibility to customize data models and workflows to fit unique business requirements. Businesses can easily configure attributes, create custom categories, and manage localized product information. UnoPim’s API allows for seamless integration with external systems, ensuring that product data is synchronized across multiple platforms like Pim, marketplaces, and ERP systems. +UnoPim offers the flexibility to customize data models and workflows to fit unique business requirements. Businesses can easily configure attributes, create custom categories, and manage localized product information. UnoPim’s API allows for seamless integration with external systems, ensuring that product data is synchronized across multiple platforms such as eCommerce storefronts, marketplaces, and ERP systems. ## Summary -In summary, UnoPim is a robust and flexible PIM platform that empowers businesses to centralize, manage, and enrich product data effectively. With its user-friendly interface, customizable features, and integration capabilities, UnoPim is the perfect solution for businesses looking to streamline product information management, enhance data accuracy, and improve time-to-market across various channels. \ No newline at end of file +In summary, UnoPim is a robust and flexible PIM platform that empowers businesses to centralize, manage, and enrich product data effectively. With its user-friendly interface, customizable features, and integration capabilities, UnoPim is the perfect solution for businesses looking to streamline product information management, enhance data accuracy, and improve time-to-market across various channels. + +## In This Section + +- **[Release Notes](release-notes)** — what shipped in v3.0.0, from headline features to breaking changes. +- **[Upgrade Guide](upgrade-guide)** — moving an existing installation from v2.1.x to v3.0.0. +- **[Patch / Minor Update Guide](patch-update)** — the repeatable checklist for `3.0.x` releases. +- **[Contribution Guide](contribution-guide)** — how to report bugs, pick a branch, and pass the checks before opening a pull request. + +New to UnoPim? Start with [Requirements](../introduction/requirements) and [Installation](../introduction/installation) instead. \ No newline at end of file diff --git a/src/3.0/prologue/patch-update.md b/src/3.0/prologue/patch-update.md index d24e03b9..4b29a974 100644 --- a/src/3.0/prologue/patch-update.md +++ b/src/3.0/prologue/patch-update.md @@ -2,10 +2,10 @@ ## Overview -Use this guide as the repeatable checklist for applying patch releases within the **2.1.x line** — for example `2.1.0 → 2.1.1` or any future `2.1.x` release. These releases are backwards compatible: no breaking API changes, no PHP or database engine bumps, and no `bootstrap/app.php` rewrites. +Use this guide as the repeatable checklist for applying patch releases within the **3.0.x line** — for example `3.0.0 → 3.0.1` or any future `3.0.x` release. These releases are backwards compatible: no breaking API changes, no PHP or database engine bumps, and no `bootstrap/app.php` rewrites. -- Moving up to the **v2.1.0** release from v2.0.x for the first time? Follow the version-specific [Upgrade Guide](upgrade-guide) instead — it lists the exact migrations, queue jobs, and configuration changes that release introduced. -- Upgrading across a **major version** (for example `1.x → 2.x`)? Use the [2.0.x Upgrade Guide](/2.0/prologue/upgrade-guide) to reach v2.0.x first, then move up to v2.1.0. +- Moving up to **v3.0.0** from v2.1.x for the first time? Follow the [Upgrade Guide](upgrade-guide) instead — v3.0.0 is a major release with breaking changes, and that guide lists the migrations, queues, and configuration changes it introduces. +- Upgrading across an older major version (for example `1.x → 2.x`)? Use the [2.0.x Upgrade Guide](/2.0/prologue/upgrade-guide) to reach v2.0.x, then the [2.1 Upgrade Guide](/2.1/prologue/upgrade-guide), and only then move to v3.0.0. ::: tip Always read the [release notes](https://github.com/unopim/unopim/releases) for the target version before applying. Even a patch release may include a new migration, queue, or config key that requires action. @@ -16,7 +16,13 @@ Always read the [release notes](https://github.com/unopim/unopim/releases) for t Even for a patch update, take a database snapshot and archive your project files. Restore is faster than debugging a partial update. ```bash +# MySQL mysqldump -u your_db_user -p your_db_name > unopim_pre_patch_backup.sql + +# PostgreSQL +pg_dump -U your_db_user your_db_name > unopim_pre_patch_backup.sql + +# Project files tar -czf unopim_pre_patch_files.tar.gz /path/to/unopim ``` @@ -39,19 +45,17 @@ php artisan queue:restart ```bash cd /path/to/unopim git fetch --tags -git checkout v2.1.1 # replace with the target 2.1.x tag -``` - -### Composer-based installs - -```bash -composer update unopim/core --with-dependencies +git checkout v3.0.1 # replace with the target 3.0.x tag ``` ### Zip / tarball installs Download the new release archive, extract it next to your existing install, and copy `.env` and `storage/` into the new directory — the same pattern as a major upgrade, just without breaking changes. +::: tip +UnoPim is distributed as an application (`unopim/unopim`), not as a library you require into your own project, so a patch update means replacing the application code — there is no single Composer package to bump. `composer create-project unopim/unopim` installs a fresh copy; the steps above update an existing one. +::: + ## 4. Reinstall Dependencies ```bash @@ -103,6 +107,7 @@ Check the release notes — only required when an index mapping changed. ```bash php artisan unopim:product:index +php artisan unopim:category:index ``` ## 9. Restart Services @@ -117,26 +122,39 @@ sudo supervisorctl start unopim-worker ## Docker Patch Update -For Docker-based installs the flow collapses into a few commands: +The flow depends on which stack you run — see [Installation with Docker](../introduction/installation-docker) for the file layout. + +### Published image stack (`compose.yaml`) + +Pull the new images and re-create the containers: ```bash cd /path/to/unopim -git fetch --tags -git checkout v2.1.1 # replace with the target 2.1.x tag -docker compose build +docker compose pull docker compose up -d -docker compose exec unopim-fpm php artisan migrate --force -docker compose exec unopim-fpm php artisan optimize:clear -docker compose exec unopim-fpm php artisan queue:restart +docker compose exec unopim php artisan migrate --force +docker compose exec unopim php artisan optimize:clear +docker compose exec unopim php artisan queue:restart ``` -For the Docker Hub setup, simply pull the new image: +### Development stack built from a checkout (`compose.dev.yaml`) + +Check out the target tag and rebuild: ```bash -docker compose -f docker-compose.hub.yml pull -docker compose -f docker-compose.hub.yml up -d +cd /path/to/unopim +git fetch --tags +git checkout v3.0.1 # replace with the target 3.0.x tag +docker compose -f compose.dev.yaml up -d --build +docker compose -f compose.dev.yaml exec unopim-fpm php artisan migrate --force +docker compose -f compose.dev.yaml exec unopim-fpm php artisan optimize:clear +docker compose -f compose.dev.yaml exec unopim-fpm php artisan queue:restart ``` +::: warning A bare `docker compose` no longer builds +Since v3.0.0, `docker compose up` in a clone resolves `compose.yaml` and pulls published images. Building from your checkout requires `-f compose.dev.yaml`. +::: + --- ## Rollback @@ -147,9 +165,12 @@ If a patch update breaks your environment, restore the previous codebase and dat # Restore files tar -xzf unopim_pre_patch_files.tar.gz -C / -# Restore database +# Restore database — MySQL mysql -u your_db_user -p your_db_name < unopim_pre_patch_backup.sql +# Restore database — PostgreSQL +psql -U your_db_user your_db_name < unopim_pre_patch_backup.sql + # Restart services sudo systemctl restart php8.4-fpm nginx sudo supervisorctl restart unopim-worker diff --git a/src/3.0/prologue/release-notes.md b/src/3.0/prologue/release-notes.md index b7981085..94a44d36 100644 --- a/src/3.0/prologue/release-notes.md +++ b/src/3.0/prologue/release-notes.md @@ -30,11 +30,17 @@ The admin panel now navigates SPA-style with AJAX and browser history, and a glo Working with the catalog is faster throughout: the product grid gains filters for category, completeness, dates, properties, and attribute values, plus saved grid views; catalog structures can be created from quick modals; a native category tree browser and lazy attribute-group loading keep large screens responsive; and products support quick export and asynchronous mass actions. -Configuration moved into a config-driven [System Settings hub](../advanced/system-settings) with package extension hooks, and each admin may now set a personal catalog locale and default channel. +Configuration moved into a config-driven [System Settings hub](../advanced/system-settings) with package extension hooks, and each admin may now set a personal catalog locale and default channel. New alongside it are an Appearance section for changing the admin logo and favicon, a System Information page covering the application, server, database, services, and installed packages, and an optional Gravatar integration. + +Product exports gained filters for channels, locales, currencies, attributes, families, status, completeness, date ranges, categories, identifiers, and attribute values. DataGrids gained cross-page "select all matching records" for mass actions. The login and recovery screens were rebuilt with Remember Me, AJAX password reset, and accessible toast notifications. + +### AI + +Magic AI moves to the `laravel/ai` package, with improved platform validation, model discovery, target-locale handling, and prompt setup. The AI Agent adds product-embedding indexing (`ai-agent:embeddings:index`) and channel-aware memory and token accounting — see [AI Agent Integration](../agentic/ai-agent). ### Platform & Developer -The platform moves to Laravel 13, PHP 8.4.1, and Symfony 8 components, with Pest 5 / PHPUnit 13 for testing. Fresh Docker environments now default to native PostgreSQL support. +The platform moves to Laravel 13, PHP 8.4.1, and Symfony 8 components, with Pest 5 / PHPUnit 13 for testing. Fresh Docker environments now default to native PostgreSQL support. Admin and installer screens no longer load TinyMCE or Inter from a CDN, so they work in environments without outbound internet access. For extension authors, 3.0 opens several new surfaces: the [Resource CRUD Kit](../packages/resource-crud-kit), the `SsoProvider` contract, variant resolver contracts, the publication `PayloadBuilder` / `PublicationGate` / type registry, and the webhook `EventRegistry`. @@ -52,4 +58,8 @@ OAuth signing keys are now per-installation 4096-bit keys, which invalidates exi The breaking changes are summarized in the [Upgrade Guide](upgrade-guide#breaking-changes): the PHP/Laravel requirement bump, OAuth token invalidation, robot-user integration ownership, variant value storage, renamed admin URLs, the webhook module replacement, the Docker layout, the deprecated `configrable-products` alias, and removed classes. -For the exhaustive list of every change, see the [CHANGELOG on GitHub](https://github.com/unopim/unopim/blob/master/CHANGELOG.md). +### Fixes + +Alongside the features above, v3.0.0 ships a large batch of fixes. The ones most likely to affect you: Docker installation failing on a fresh clone, the pre-built image stack serving nothing, the committed OAuth signing keys being replaced by per-installation keys, and SSRF protection on Magic AI connection testing and model discovery. + +For the exhaustive list of every change, see the [CHANGELOG for v3.0.0](https://github.com/unopim/unopim/blob/v3.0.0/CHANGELOG.md). diff --git a/src/3.0/prologue/upgrade-guide.md b/src/3.0/prologue/upgrade-guide.md index 9a34173b..f5b60bb4 100644 --- a/src/3.0/prologue/upgrade-guide.md +++ b/src/3.0/prologue/upgrade-guide.md @@ -112,10 +112,10 @@ sudo systemctl restart php8.4-fpm nginx sudo supervisorctl restart unopim-worker ``` -Passports and publications run on a dedicated queue — add it to your worker if you enable Digital Product Passports: +Passports, publications, and webhook deliveries run on dedicated queues — your worker must list them, or that work is queued and never processed: ```bash -php artisan queue:work --queue="system,completeness,publication,default" +php artisan queue:work --queue="system,completeness,publication,webhooks,default" ``` ## Breaking Changes @@ -151,7 +151,7 @@ Hardcoded admin URLs in extensions or bookmarks must be updated: | `catalog/attributegroups/*` | `catalog/attribute-groups/*` | | `catalog/families/*` | `catalog/attribute-families/*` | | `settings/data-transfer/*` | `data-transfer/*` | -| `tracker/*` | `job-tracker/*` | +| `settings/data-transfer/tracker/*` | `data-transfer/job-tracker/*` | | `integrations/api-keys/*` | `configuration/integrations/*` | | legacy combined settings page | `configuration/system-settings` hub (`configuration/system/{key}` editors) | @@ -174,6 +174,10 @@ The single-webhook configuration was replaced by the multi-webhook module. `Webh The misspelled `configrable-products` endpoint still works in this release but returns deprecation and successor headers. Move clients to `configurable-products` before the alias is removed in a future release. +### Other API client changes + +Permissions are now enforced on reads as well as writes, error responses share a single shape, rate limits are enforced, and `limit` is capped at 100. If you maintain a client built against the v2.x API, work through [Migrating an API Client to v3.0](../api/migrating-your-client) — it covers each change and what the client must do about it. + ### Removed classes and services - `Webkul\Admin\Helpers\Reporting`, `Webkul\Admin\Http\Resources\AttributeResource`, `AttributeOptionResource`, `Webkul\Admin\Listeners\Base` @@ -204,7 +208,7 @@ For the complete list, see the [UnoPim CHANGELOG on GitHub](https://github.com/u 1. **Re-authenticate API clients** — issue new tokens; old ones are invalid. 2. **Test core functionality** — log in, browse products, categories, attributes. 3. **Check error logs** — review `storage/logs/laravel.log`. -4. **Verify queue processing** — confirm workers process the `system`, `completeness`, and (if passports are enabled) `publication` queues. +4. **Verify queue processing** — confirm workers process the `system`, `completeness`, `webhooks`, and (if passports are enabled) `publication` queues. 5. **Re-index Elasticsearch** — if enabled: ```bash From c186e3d0ce7d503f494c1e4843d4805c1c004f33 Mon Sep 17 00:00:00 2001 From: Navneet Kumar Date: Wed, 12 Aug 2026 21:14:05 +0530 Subject: [PATCH 2/3] docs(3.0): address review feedback on the accuracy pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pagination envelope documented in `api/explanation.md` was wrong: it described `data`/`links`/`meta`, but `ApiDataSource::responseFormatData()` returns the counters at the top level next to a four-URL `links` object and emits no `meta` at all. The page now matches, and documents the `pagination_type=search_after` cursor mode it never mentioned. Passports are the one exception — they go through a Laravel resource collection and really do return `links`/`meta` — so that page now warns against sharing a pagination parser with the rest of the API. The association-type and variant-structure examples were missing the envelope entirely; both now show what the endpoints return. Product associations are asymmetric by design: a request identifies the linked product with `sku`, a response returns `related_sku`. Saying "the same structure" hid that. The Nginx dot-file deny also swallowed `/.well-known/acme-challenge/`, breaking Let's Encrypt HTTP-01 renewal. An allow rule now precedes it in all four vhosts, not only the one the review flagged. --- src/3.0/advanced/events.md | 2 +- src/3.0/api/association_types.md | 8 ++- src/3.0/api/explanation.md | 66 ++++++++++--------- src/3.0/api/migrating-your-client.md | 2 +- src/3.0/api/passports.md | 4 ++ src/3.0/api/product.md | 2 +- src/3.0/api/variant_structures.md | 11 +++- src/3.0/introduction/installation-centos.md | 6 ++ src/3.0/introduction/installation-debian.md | 6 ++ src/3.0/introduction/installation-ubuntu.md | 6 ++ .../introduction/web-server-configuration.md | 8 +++ .../store-data-through-repositories.md | 2 +- 12 files changed, 85 insertions(+), 38 deletions(-) diff --git a/src/3.0/advanced/events.md b/src/3.0/advanced/events.md index 5af220b7..8346619c 100644 --- a/src/3.0/advanced/events.md +++ b/src/3.0/advanced/events.md @@ -11,7 +11,7 @@ In UnoPim, events and listeners are organized in a clear and structured manner: This organization makes it easy to manage and locate the event-driven components of your application. -To learn in detail about Controllers, you can visit the Laravel documentation [here](https://laravel.com/docs/13.x/events). +To learn in detail about events, you can visit the Laravel documentation [here](https://laravel.com/docs/13.x/events). ## Creating an Event Class diff --git a/src/3.0/api/association_types.md b/src/3.0/api/association_types.md index 75fd65d3..0f109754 100644 --- a/src/3.0/api/association_types.md +++ b/src/3.0/api/association_types.md @@ -55,7 +55,13 @@ The endpoint accepts these query parameters: ], "current_page": 1, "last_page": 1, - "total": 2 + "total": 2, + "links": { + "first": "http://127.0.0.1:8000/api/v1/rest/association-types?page=1", + "last": "http://127.0.0.1:8000/api/v1/rest/association-types?page=1", + "next": null, + "prev": null + } } ``` ::: diff --git a/src/3.0/api/explanation.md b/src/3.0/api/explanation.md index 7eb707cb..9a78a4c9 100644 --- a/src/3.0/api/explanation.md +++ b/src/3.0/api/explanation.md @@ -1,14 +1,16 @@ # Response Structure Explained -Every paginated response in this API shares the same shape, and this page walks you through it. Once you understand the three top-level keys — `data`, `links`, and `meta` — you can navigate any list endpoint. +Every paginated response in this API shares the same shape, and this page walks you through it. Once you understand the top-level keys — `data`, `current_page`, `last_page`, `total`, and `links` — you can navigate any list endpoint. Taking the categories endpoint as an example, a paginated response looks like this: ~~~json { "data": [{...},{...},...,{...}], - "links": {...}, - "meta": {...} + "current_page": 1, + "last_page": 2, + "total": 10, + "links": {...} } ~~~ @@ -16,16 +18,28 @@ Taking the categories endpoint as an example, a paginated response looks like th The `data` key holds the collection of records themselves — in this example, the UnoPim store's categories. Its shape matches the single-record response of the same resource. +## The Pagination Counters + +The counters sit at the top level of the response, not inside a nested object: + + | Name | Info | + | ------------- | -------------------------------------------------------------------------------------------------- | + | current_page | Display the current page number. | + | last_page | Display the last page number. | + | total | Display the total number of records matching the request. | + +Use the `limit` query parameter to set the page size — it defaults to `10` and is clamped to a maximum of `100` — and `page` to choose the page. + ## The Links Object The `links` key gives you ready-made URLs for moving between pages: ~~~json "links": { - "first": "https://example.com/api/categories?limit=5&pagination=342234&page=1", - "last": "https://example.com/api/categories?limit=5&pagination=342234&page=2", - "prev": null, - "next": "https://example.com/api/categories?limit=5&pagination=342234&page=2" + "first": "https://example.com/api/v1/rest/categories?limit=5&page=1", + "last": "https://example.com/api/v1/rest/categories?limit=5&page=2", + "next": "https://example.com/api/v1/rest/categories?limit=5&page=2", + "prev": null } ~~~ @@ -35,33 +49,21 @@ Each link serves a distinct purpose: | ------------- | ------------------------------------------------------------------------------------------------------------------- | | first | Display the first url link of the called API with filter variable. | | last | Display the last url link of the called API with filter variable. | - | prev | Display the previous url of the current called API url. | + | prev | Display the previous url of the current called API url. If no previous url available then it will contain `null`. | | next | Display the next url of the current called API url. If no next url available then it will contain the `null` value. | -## The Meta Object +## Cursor Pagination -The `meta` key appears only on paginated responses and describes where you are in the result set: +Deep pages get slower as the offset grows, because the database still has to walk every skipped row. For large catalogs, pass `pagination_type=search_after` to page by cursor instead. The response drops the counters — computing `total` and `last_page` requires the very `COUNT(*)` this mode exists to avoid — and returns a cursor instead: - ~~~json - "meta": { - "current_page": 1, - "from": 1, - "last_page": 2, - "path": "https://example.com/api/categories", - "per_page": "5", - "to": 5, - "total": 10 - } - ~~~ - -Here is what each field tells you: +~~~json +{ + "data": [{...},{...},...,{...}], + "search_after": 1240, + "links": { + "next": "https://example.com/api/v1/rest/products?pagination_type=search_after&search_after=1240" + } +} +~~~ - | Name | Info | - | ------------- | -------------------------------------------------------------------------------------------------- | - | current_page | Display the current page number. | - | from | Display the first count of the returned data object based on the provided page and limit filters. | - | last_page | Display the last page number. | - | path | Display the current api url without input parameters. | - | per_page | Display the total of records in a single page. | - | to | Display the last count of the returned data object based on the provided page and limit filters. | - | total | Display the total number of records in the store. | +Follow `links.next` until `search_after` comes back as `null`, which marks the end of the result set. diff --git a/src/3.0/api/migrating-your-client.md b/src/3.0/api/migrating-your-client.md index f90de56b..882f6e03 100644 --- a/src/3.0/api/migrating-your-client.md +++ b/src/3.0/api/migrating-your-client.md @@ -205,7 +205,7 @@ A single product `GET` now returns an `associations` block alongside the existin This is additive. The `values.associations` SKU lists a v2.x client already reads are unchanged, and the listing endpoint does not include the block at all — it is returned only for a single product, to keep list responses free of a per-row query. -The same structure may be sent on create and update, under a top-level `associations` key: +The same block may be sent on create and update, under a top-level `associations` key. The key naming differs by direction: write `sku`, read `related_sku`. ```json { diff --git a/src/3.0/api/passports.md b/src/3.0/api/passports.md index 4e0d51bc..aeb2da7e 100644 --- a/src/3.0/api/passports.md +++ b/src/3.0/api/passports.md @@ -53,6 +53,10 @@ GET {{url}}/api/v1/rest/passports ``` ::: +::: warning Envelope differs from the rest of the API +The passport endpoints are built on a Laravel resource collection, so they wrap pagination in `links` and `meta`. Every other list endpoint returns the counters at the top level instead — see [Response Structure Explained](./explanation). Do not share one pagination parser between them. +::: + ## Read a Product's Publications Every publication for one product, newest first, unpaginated. diff --git a/src/3.0/api/product.md b/src/3.0/api/product.md index 4b9fff64..9a7bd7a3 100644 --- a/src/3.0/api/product.md +++ b/src/3.0/api/product.md @@ -341,7 +341,7 @@ A single-product `GET` returns an extra `associations` block alongside `values`. The block is returned only for a single product, never on the listing, so a paginated response does not run one query per row. The legacy `values.associations` SKU lists are unchanged. -You may send the same structure when creating or updating a product, under a top-level `associations` key: +The same block may be sent when creating or updating a product, under a top-level `associations` key. Note the one difference: a request identifies the linked product with `sku`, while a response returns it as `related_sku`. ```json { diff --git a/src/3.0/api/variant_structures.md b/src/3.0/api/variant_structures.md index ae16c982..5580c005 100644 --- a/src/3.0/api/variant_structures.md +++ b/src/3.0/api/variant_structures.md @@ -54,7 +54,16 @@ GET {{url}}/api/v1/rest/families/{code}/variant-structures "created_at": "2026-07-22T10:14:03.000000Z", "updated_at": "2026-07-22T10:14:03.000000Z" } - ] + ], + "current_page": 1, + "last_page": 1, + "total": 1, + "links": { + "first": "http://127.0.0.1:8000/api/v1/rest/families/apparel/variant-structures?page=1", + "last": "http://127.0.0.1:8000/api/v1/rest/families/apparel/variant-structures?page=1", + "next": null, + "prev": null + } } ``` ::: diff --git a/src/3.0/introduction/installation-centos.md b/src/3.0/introduction/installation-centos.md index 0b5b6602..b7185968 100644 --- a/src/3.0/introduction/installation-centos.md +++ b/src/3.0/introduction/installation-centos.md @@ -426,6 +426,12 @@ server { fastcgi_buffer_size 32k; } + # ACME HTTP-01 validation must stay reachable, so allow it before the dot-file deny + location ^~ /.well-known/acme-challenge/ { + allow all; + try_files $uri =404; + } + # Deny access to hidden files location ~ /\. { deny all; diff --git a/src/3.0/introduction/installation-debian.md b/src/3.0/introduction/installation-debian.md index 29934006..761bdcf6 100644 --- a/src/3.0/introduction/installation-debian.md +++ b/src/3.0/introduction/installation-debian.md @@ -380,6 +380,12 @@ server { fastcgi_buffer_size 32k; } + # ACME HTTP-01 validation must stay reachable, so allow it before the dot-file deny + location ^~ /.well-known/acme-challenge/ { + allow all; + try_files $uri =404; + } + # Deny access to hidden files location ~ /\. { deny all; diff --git a/src/3.0/introduction/installation-ubuntu.md b/src/3.0/introduction/installation-ubuntu.md index 0aacdc25..72cc534e 100644 --- a/src/3.0/introduction/installation-ubuntu.md +++ b/src/3.0/introduction/installation-ubuntu.md @@ -425,6 +425,12 @@ server { fastcgi_buffer_size 32k; } + # ACME HTTP-01 validation must stay reachable, so allow it before the dot-file deny + location ^~ /.well-known/acme-challenge/ { + allow all; + try_files $uri =404; + } + # Deny access to hidden files location ~ /\. { deny all; diff --git a/src/3.0/introduction/web-server-configuration.md b/src/3.0/introduction/web-server-configuration.md index 85c6c13f..537655b3 100644 --- a/src/3.0/introduction/web-server-configuration.md +++ b/src/3.0/introduction/web-server-configuration.md @@ -144,6 +144,14 @@ server { return 404; } + # ────────────────────────────────────────────── + # ACME HTTP-01 validation (must precede the dot-file deny) + # ────────────────────────────────────────────── + location ^~ /.well-known/acme-challenge/ { + allow all; + try_files $uri =404; + } + # ────────────────────────────────────────────── # Security: Deny access to hidden files (.env, .git, etc.) # ────────────────────────────────────────────── diff --git a/src/3.0/packages/store-data-through-repositories.md b/src/3.0/packages/store-data-through-repositories.md index 0d8b69f9..ce718948 100644 --- a/src/3.0/packages/store-data-through-repositories.md +++ b/src/3.0/packages/store-data-through-repositories.md @@ -23,7 +23,7 @@ Manually setting up repository files involves creating and organizing repository ### Setting Up ExampleRepository in Webkul/Example Package -Start by creating a `Repositories` folder within the `Webkul/Example/src/` directory. This folder will house the repository class responsible for handling example-related database operations.Create a file named `ExampleRepository.php`. +Start by creating a `Repositories` folder within the `Webkul/Example/src/` directory. This folder will house the repository class responsible for handling example-related database operations. Create a file named `ExampleRepository.php`. ``` └── packages From 71adab374744e7eaea56987b8cc35e1492f7cc36 Mon Sep 17 00:00:00 2001 From: Navneet Kumar Date: Wed, 19 Aug 2026 17:48:44 +0530 Subject: [PATCH 3/3] fix(docs): keep the version switcher off 404 pages The switcher's regex listed every release except 3.0, so on a 3.0 page it matched nothing and dropped the rest of the path: picking 2.1 navigated to /2.1/, which has no index page. Match versions generically, and fall back to a version's landing page when the current page does not exist there. --- .vitepress/theme/components/VersionSelect.vue | 46 ++++++++++++++----- .vitepress/theme/pages.data.mts | 9 ++++ 2 files changed, 43 insertions(+), 12 deletions(-) create mode 100644 .vitepress/theme/pages.data.mts diff --git a/.vitepress/theme/components/VersionSelect.vue b/.vitepress/theme/components/VersionSelect.vue index d61ad042..012f5f9f 100644 --- a/.vitepress/theme/components/VersionSelect.vue +++ b/.vitepress/theme/components/VersionSelect.vue @@ -18,8 +18,10 @@ diff --git a/.vitepress/theme/pages.data.mts b/.vitepress/theme/pages.data.mts new file mode 100644 index 00000000..653f5382 --- /dev/null +++ b/.vitepress/theme/pages.data.mts @@ -0,0 +1,9 @@ +import { createContentLoader } from 'vitepress' + +/** + * Every documented page URL, used by the version switcher to fall back to a + * version's landing page instead of navigating to a path that version lacks. + */ +export default createContentLoader('*/**/*.md', { + transform: (raw) => raw.map(({ url }) => url) +})