# Product Lifecycle Hooks

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

The eight hooks over creating, updating, deleting and switching a product.

## Overview

A product travels three steps: it is created, updated and deleted. Each has a gate you can stop at in front of it and an event that reports it behind.

Two status hooks sit beside them. The single one fires per product; the bulk one fires **once** when an action reaches many products from the list.

## Reference

### Stopping a product being created

gateproduct.create

`AdminProducts` before the write

Runs before a new product is created.

Parameters 2

$inputarrayThe form input: type, group, category, name, module and address.

$statearrayContext: the group keys, the default currency and the language keys.

Return 1

string|null**A non-empty text blocks the operation** and is shown to the administrator as the error. An empty return lets it carry on.

Listener PHP

```php
Hook::add('gate:product.create', 10, function ($input, $state) {
    if (!Acme::namingOk($input['name'] ?? '')) return 'The product name breaks the naming rule.';

    return null;
});
```

### Following a product being created

actionproduct.created

`AdminProducts` zero means it failed

Runs after a product is created.

Parameters 2

$new_idintThe id of the new product. ? **A zero means no product was created**. A listener that uses the id straight away then works on a record that is not there.

$inputarrayThe creation input: type, group, category, name, module and address.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:product.created', 10, function ($new_id, $input) {
    // ZERO = no product was created.
    if (!$new_id) return;

    Acme::registerCatalogItem($new_id, $input['name'] ?? '');
});
```

### Stopping a product update

gateproduct.update

`AdminProducts` before and after in hand

Runs before a product is updated. You hold both the new data and the old record, so you can compare what changed.

Parameters 3

$idintThe product id.

$inputarrayThe new data about to be written.

$detailarrayThe record as it was **before** the change.

Return 1

string|null**A non-empty text blocks the operation** and is shown to the administrator as the error. An empty return lets it carry on.

Listener PHP

```php
Hook::add('gate:product.update', 10, function ($id, $input, $detail) {
    // You hold both records: measure the difference.
    if (($detail['module'] ?? '') !== ($input['module'] ?? '') && Acme::hasLiveServices($id))
        return 'The module cannot change while live services run on it.';

    return null;
});
```

### Following a product update

actionproduct.updated

`AdminProducts` before and after in hand

Runs after the product is updated.

Parameters 3

$idintThe product id.

$inputarrayThe full edit input: pricing, languages, module data and resource limits.

$detailarrayThe record from **before** the update.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:product.updated', 10, function ($id, $input, $detail) {
    Acme::syncCatalog($id, $input, $detail);
});
```

### Stopping a product deletion

gateproduct.delete

`AdminProducts` before the write

Runs before a product is deleted.

Parameters 2

$idintThe id of the product to be deleted.

$detailarrayThe product record: type, module and group.

Return 1

string|null**A non-empty text blocks the operation** and is shown to the administrator as the error. An empty return lets it carry on.

Listener PHP

```php
Hook::add('gate:product.delete', 10, function ($id, $detail) {
    if (Acme::hasLiveServices($id)) return 'Live services still run on this product.';

    return null;
});
```

### Following a product deletion

actionproduct.deleted

`AdminProducts` after deletion

Runs after the product is deleted.

Parameters 2

$product_idintThe id of the deleted product.

$detailarrayIts last state. The record is gone: take what you need from here.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:product.deleted', 10, function ($product_id, $detail) {
    Acme::dropCatalogItem($product_id);
});
```

### Following a product status change

actionproduct.status_changed

`AdminProducts` one product

Runs when a product is opened for sale or closed.

Parameters 2

$product_idintThe product id.

$statusstringThe new state: `active` or `inactive`.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:product.status_changed', 10, function ($product_id, $status) {
    Acme::setCatalogVisible($product_id, $status === 'active');
});
```

### Following a bulk action

actionproduct.bulk_action_applied

`AdminProducts` one call, many products

Runs when an action is applied to several products at once from the list. Unlike the single status event, this is **one call**, not one per product.

Parameters 2

$actionstringThe action applied: `active` or `inactive`.

$idsarrayThe ids of the products handled.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:product.bulk_action_applied', 10, function ($action, $ids) {
    // One call, many products: the single status event does not fire here.
    Acme::bulkVisibility($ids, $action === 'active');
});
```

## Pitfalls

> **A zero in the create event means it failed**
> 
> The new product id can arrive as **zero**, which means no product was created. A listener that uses the id straight away then works on a record that is not there. Check it on the first line.

> **A bulk action does not raise the single status event**
> 
> When an action reaches many products from the list, **only the bulk event** fires. Work hung on the single status hook never runs on that path; listen on both.

## 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)
