Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- Add optional `status` column for product imports and exports.
- Refactor the Export Product sidebar button.
- Set `promotable` to true by default
- Rewrote docs

## 2.0.5 - 2026-02-19

Expand Down
60 changes: 56 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,60 @@
![Screenshot](resources/img/header.png)
![Variant Manager](resources/img/header.png)

# Variant Manager

A plugin for managing products and their variants in Craft Commerce.
A Craft CMS plugin that imports and exports Craft Commerce product **variants** from CSV files.

## What it does

- Imports a CSV to create or update a Craft Commerce product and its variants.
- Bulk-imports many products at once from a zip of CSVs, each file becoming its own product.
- Exports a product to CSV from the product edit page, or many products at once from the Variants element index.
- Adds a **Variant Attributes** field that stores option name and value pairs (Color, Size, Material) on each variant for filtering on the storefront.
- Logs each import and export, with configurable retention, in a dashboard activity feed.

## Requirements

- Craft CMS `^5.0`
- Craft Commerce `^5.0`
- PHP `^8.2`

## Install

```sh
composer require fostercommerce/variant-manager
./craft plugin/install variant-manager
```

See [`docs/installation.md`](./docs/installation.md) for the full installation and configuration guide.

## Importing

Upload a CSV (or a zip of CSVs) from **Variant Manager -> Dashboard**. The CSV's filename determines the product: a new filename creates a new product, an existing product title updates that product. Each row becomes one variant. Columns map to product fields, variant fields, per-site Commerce fields, inventory levels, and variant attributes.

See [`docs/user-guide/importing.md`](./docs/user-guide/importing.md) and [`docs/user-guide/csv-format.md`](./docs/user-guide/csv-format.md).

## Exporting

Two ways to export: from a single product's edit page (sidebar **Export Product** button), or from the **Variants** element index using the **Export Variant Data** action on a multi-select. A single product downloads as one CSV; multiple products download as a zip. Exported CSVs are shaped so they can be reimported without edits to the column headers.

See [`docs/user-guide/exporting.md`](./docs/user-guide/exporting.md).

## Variant Attributes field

The plugin ships a **Variant Attributes** field type that you add to each product type's variant field layout. The field stores the option-name and option-value pairs from your CSV (Color: Red, Size: Small) as JSON on the variant, and exposes them to Twig for variant selectors and faceted filtering.

See [`docs/reference/field-type.md`](./docs/reference/field-type.md) for storage and Twig usage.

## Permissions

In addition to `accessPlugin-variant-manager`:

- `variant-manager:import`, upload CSVs and create or update products and variants.
- `variant-manager:export`, export products from the product edit page or the variants index.
- `variant-manager:manage`, clear the activity log and manage plugin data.

See [`docs/reference/permissions.md`](./docs/reference/permissions.md).

## Setup and Usage
## License

[See docs](/docs)
Proprietary.
25 changes: 0 additions & 25 deletions docs/README.md

This file was deleted.

74 changes: 74 additions & 0 deletions docs/dev-guide/custom-queue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Custom queue

How to keep Variant Manager's import jobs from delaying other Craft queue work. Audience: developers running production sites with high queue volume.

Bulk imports can generate thousands of import jobs. By default they go on Craft's main queue, which means other Craft work (search index rebuilds, image transforms, emails) can sit behind a long import batch.

Two ways to address this: lower the priority of Variant Manager jobs, or send them to a custom queue.

## Lower job priority

In a site module, listen for `Queue::EVENT_BEFORE_PUSH` and bump the priority of `ImportJob` instances. Higher priority numbers run later.

```php
use fostercommerce\variantmanager\jobs\Import as ImportJob;
use yii\base\Event;
use yii\queue\PushEvent;
use yii\queue\Queue;

Event::on(
Queue::class,
Queue::EVENT_BEFORE_PUSH,
static function (PushEvent $event): void {
if ($event->job instanceof ImportJob) {
// UpdateSearchIndex jobs have a priority of 2048; pushing above that means imports run after search updates.
$event->priority = 2049;
}
}
);
```

Wire this into your module's `init()` method.

## Run imports on a dedicated queue

Configure Variant Manager to push its jobs to a separate Yii queue, so they run on a different worker (or run alongside the main queue without blocking it).

In `config/app.php`:

```php
return [
'bootstrap' => ['priorityQueue'],
'components' => [
'plugins' => [
'pluginConfigs' => [
'variant-manager' => [
'queue' => 'priorityQueue',
],
],
],
'priorityQueue' => [
'class' => \craft\queue\Queue::class,
'channel' => 'priority',
],
],
];
```

The string `priorityQueue` is the component handle Variant Manager will resolve at runtime; it can be anything as long as the component is registered.

Then run the worker for the custom queue separately:

```sh
./craft queue/run --queue=priorityQueue
```

See Craft's [custom queues guide](https://craftcms.com/docs/5.x/system/queue.html#custom-queues) for more on how Yii's queue components are wired up.

## Verifying it works

1. Upload a small CSV from **Variant Manager -> Dashboard**. The upload modal returns "File ... has been queued for processing" as usual.
2. With the main queue worker stopped, check the dashboard activity log; the new row stays in its pending state because the main queue does not own the job.
3. Start the custom queue worker: `./craft queue/run --queue=priorityQueue`.
4. Refresh the dashboard. The activity log row flips to the green-dot success state once the worker drains the import.
5. For the priority approach, push two jobs back to back (an import and a search index rebuild) and confirm the search rebuild runs first.
69 changes: 69 additions & 0 deletions docs/dev-guide/template-tags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Template tags

Twig helpers Variant Manager exposes on the `craft` variable for use in storefront templates.

## `craft.variantManager.getAttributeOptions(product, only?)`

Returns the distinct attribute names and the unique values used across a product's variants. Useful for building variant pickers and faceted filters.

Parameters:

- `product`: a `Product` element or a product ID.
- `only` (optional): a string or array of attribute names to limit the result to.

Returns an array of associative arrays, each with:

- `name`: the attribute name.
- `values`: a deduplicated array of every value used by any variant for that attribute.

### Example output

```twig
{% set attributeOptions = craft.variantManager.getAttributeOptions(product.id) %}
{{ attributeOptions | json_encode }}
```

Renders something like:

```json
[
{ "name": "Color", "values": ["Red", "Blue"] },
{ "name": "Size", "values": ["Small", "Medium", "Large"] }
]
```

### Example: build a radio picker for every attribute

```twig
{% set product = craft.products().id(30).one() %}

{% for attribute in craft.variantManager.getAttributeOptions(product) %}
<fieldset>
<legend>{{ attribute.name }}</legend>
{% for value in attribute.values %}
<label>
<input type="radio" name="{{ attribute.name|kebab }}" value="{{ value }}">
{{ value }}
</label>
{% endfor %}
</fieldset>
{% endfor %}
```

### Example: limit to specific attributes

```twig
{% set colorsAndSizes = craft.variantManager.getAttributeOptions(product, ['Color', 'Size']) %}
```

Pass a single string for one attribute:

```twig
{% set colors = craft.variantManager.getAttributeOptions(product, 'Color') %}
```

## Related

- [Querying variants](./twig-queries.md), filtering `craft.variants()` by attribute values.
- [Add to cart recipe](../recipes/add-to-cart.md), a full variant picker that adds to cart.
- [Variant filter recipe](../recipes/variant-filter.md), client-side filtering examples.
103 changes: 103 additions & 0 deletions docs/dev-guide/twig-queries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Querying variants

How to filter Commerce variants by their Variant Attributes field. Audience: developers building storefront templates or PHP code that queries variants.

Substitute the handle you gave your Variant Attributes field (`variantAttributes`, `myVariantAttributes`, anything you chose) for `variantAttributes` in the examples below.

## Three filter shapes

The Variant Attributes field accepts:

1. A string. Returns variants that have **any** attribute with that value.
2. An associative array. Returns variants that match **every** name/value pair.
3. A list of strings and/or associative arrays. Returns variants that match **any** of the entries.

## Filter by an option value

Find variants whose Variant Attributes contains the value `Red` under any attribute name.

PHP:

```php
\craft\commerce\elements\Variant::find()
->variantAttributes('Red')
->all();
```

Twig:

```twig
{% set variants = craft.variants().variantAttributes('Red').all() %}
```

## Filter by all attribute and option pairs (AND)

Find variants that have `Color = Red` AND `Size = Small`.

PHP:

```php
\craft\commerce\elements\Variant::find()
->variantAttributes([
'Color' => 'Red',
'Size' => 'Small',
])
->all();
```

Twig:

```twig
{% set filter = {
'Color': 'Red',
'Size': 'Small'
} %}
{% set variants = craft.variants().variantAttributes(filter).all() %}
```

## Filter by any attribute and option pair (OR)

Find variants that match any of the entries in a list. Each entry can be a value-only string or a `Name => Value` pair.

PHP:

```php
\craft\commerce\elements\Variant::find()
->variantAttributes([
['Color' => 'Red'],
'Cotton',
['Size' => 'Small'],
])
->all();
```

Twig:

```twig
{% set filter = [
{ 'Color': 'Red' },
'Cotton',
{ 'Size': 'Small' }
] %}
{% set variants = craft.variants().variantAttributes(filter).all() %}
```

## How it works

The field stores attributes as JSON. The query builder generates database conditions tailored to your database:

- MySQL: `json_search()` against the field's JSON path, with one condition per name/value pair.
- PostgreSQL: `@>` containment against the JSON column.

You do not need to do anything special to enable this; both paths are picked automatically.

## Errors

- `$value items must be associative arrays or strings`: a list entry was neither. Check that every element of the array is a string or an `{ name: value }` map.
- `$value must be either an array or a string`: a non-string, non-array value was passed (a number, a Date, an Element). Convert to a string before passing.

## Related

- [Template tags](./template-tags.md), the `getAttributeOptions` helper.
- [Add to cart recipe](../recipes/add-to-cart.md).
- [Variant filter recipe](../recipes/variant-filter.md).
Loading
Loading