# Product Data and Catalogue Hooks

https://dev.wisecp.com/es/product-data-hooks

The eight hooks over product data, the catalogue, the card lists and stock.

## Overview

Product data flows two ways: through the save filter **on the way in**, and through the read filter **on the way out**. The second is a hot path and its result goes into the cache.

The rest build the lists a customer sees: catalogue plans, related products, software cards. The stock hook stands apart; it is moved not by an administrator but by an **order**.

## Reference

### Changing the product data before it is saved

filterproduct.save_data

`AdminProducts` passed by link

Runs before the product data is written. This is how you hold one pricing policy in one place.

Parameters 2

$inputarrayby linkThe data to be written: type, options, module data, languages, pricing and tax.

$detailarrayThe existing record before the change.

Return 1

voidThe return is ignored; you write over the data.

Listener PHP

```php
Hook::add('filter:product.save_data', 10, function (&$input, $detail) {
    // Hold the pricing policy in one place.
    $input['pricing'] = Acme::applyMargin($input['pricing'] ?? []);
});
```

### Changing a product as it is read

filterproduct.get

`Products::get` the result is cached

Runs after a product is read from the database. **Every** product read passes here: the catalogue, the basket, the service detail.

Parameters 2

$productarrayby linkThe resolved product row with its options and module data unpacked. When no product was found an **empty array** arrives; look before you reach into it.

$contextarrayThe id asked for and the language.

Return 1

voidThe return is ignored; you write over the product. ? The array you change **goes into the cache**: a mistake here shows up not on one read but on every read that follows.

Listener PHP

```php
Hook::add('filter:product.get', 10, function (&$product, $context) {
    // A product that was not found arrives as an EMPTY array.
    if (!$product) return;

    $product['acme_badge'] = Acme::badgeFor((int) ($context['id'] ?? 0));
});
```

### Changing the product configuration fields

filterproduct.config_options

`AdminProducts` empty means no form

Runs before the configuration fields offered by the module appear.

Parameters 3

$config_optionsarrayby linkThe field definitions to be shown.

$moduleobjectThe product module instance.

$module_dataarrayThe current module data of the product.

Return 1

voidThe return is ignored; you write over the definitions. If the array is **left empty** after the filter, no form appears at all: clearing the fields removes the form.

Listener PHP

```php
Hook::add('filter:product.config_options', 10, function (&$config_options, $module, $module_data) {
    // Emptying the array removes the form entirely.
    unset($config_options['legacy_option']);
});
```

### Changing the catalogue plans

filterproduct.catalog_plans

`website/products` passed by link

Runs once the plan list on the catalogue page is prepared. Ordering, hiding and badging belong here.

Parameters 2

$plansarrayby linkThe plan list.

$ctxarrayContext: the category, the product kind and the layout.

Return 1

voidThe return is ignored; you write over the data.

Listener PHP

```php
Hook::add('filter:product.catalog_plans', 10, function (&$plans, $ctx) {
    $plans = Acme::sortByPopularity($plans);
});
```

### Changing the related product cards

filterproduct.related_products

`website/products` passed by link

Runs once the related product list under a product detail is built. The core default is up to four products from the same catalogue.

Parameters 2

$relatedarrayby linkThe card list.

$idintby linkThe id of the product on screen; this is the "related to what" context.

Return 1

voidThe return is ignored; you write over the data.

Listener PHP

```php
Hook::add('filter:product.related_products', 10, function (&$related, &$id) {
    $related = Acme::recommendFor($id) ?: $related;
});
```

### Changing the software card list

filterproduct.software_list

`website/products` passed by link

Runs once the card list of the software store is prepared.

Parameters 2

$outarrayby linkThe card list; each card carries a title, category, image and tags.

$ctxarrayContext: the currency the cards are priced in. The currency varies by visitor: if you produce a price, use this value rather than the default.

Return 1

voidThe return is ignored; you write over the data.

Listener PHP

```php
Hook::add('filter:product.software_list', 10, function (&$out, $ctx) {
    // If you produce a price, use the currency from the context.
    $out = Acme::attachOffers($out, (int) ($ctx['currency'] ?? 0));
});
```

### Following a stock change

actionproduct.stock_changed

`Orders` driven by an order

Runs when the stock of a product moves. The move is not manual: an **order** becoming active, or ceasing to be, drives it.

Parameters 4

$product_idintThe product id.

$deltaintWhich way it moved: **minus one** means an order became active and stock came down, **plus one** means an order left and stock returned.

$new_stockintThe new stock written. It never goes below zero: expect no negative value.

$order_idintThe order that drove the change.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:product.stock_changed', 10,
    function ($product_id, $delta, $new_stock, $order_id) {
        // Stock never goes below zero.
        if ($new_stock === 0) Acme::alertOutOfStock($product_id);
    });
```

### Changing per-country message pricing

filterproduct.intl_sms_country_pricing_save

`AdminProducts` passed by link

Runs before international text message prices are saved.

Parameters 1

$current_listarrayby linkCountry code against price: cost, selling amount, currency and status.

Return 1

voidThe return is ignored; you write over the data.

Listener PHP

```php
Hook::add('filter:product.intl_sms_country_pricing_save', 10, function (&$current_list) {
    foreach ($current_list as $code => $row)
        $current_list[$code]['amount'] = Acme::markup((float) ($row['cost'] ?? 0));
});
```

## Pitfalls

> **The read filter result goes into the cache**
> 
> A change you make in the product read filter is **kept** and comes back on later reads. A mistake here shows up not on one page but everywhere until the cache is cleared.

> **An emptied configuration removes the form**
> 
> If the array is empty after the configuration fields filter, **no form appears at all**. Clearing everything while meaning to hide one field leaves the administrator unable to configure the product.

## Related Articles

- Product Hooks
- [Service Hooks](https://dev.wisecp.com/en/service-lifecycle-hooks)
- [How Hooks Work](https://dev.wisecp.com/en/how-hooks-work)
