Optionline documentation1.x
Developers

Filters and hooks

Read options in code, add field types and settings, add fields to products, decide when a field applies, check values and work out prices.

Optionline's hooks are WordPress filters and actions. Put the examples in a small custom plugin or your child theme's functions.php. Optionline Pro is built on these same hooks.

How a field looks in code

A product's own fields are stored, in order, in its _optionline_fields meta. Read them with Extendiment\Optionline\Fields::get( $product ), or the fields a product actually shows (including fields add-ons add, without those that can't be used) with Fields::for_product( $product ).

array(
	'id'          => 'engraving',  // stable: cart lines and orders point at it
	'label'       => 'Engraving',
	'type'        => 'text',       // text, textarea, select, radio, checkboxes, or an add-on's type
	'required'    => true,
	'description' => 'Engraved under the handle.',
	'placeholder' => 'Up to 16 letters',
	'price'       => '6',          // types without choices: added when it's filled in
	'choices'     => array(),      // types with choices: array( 'id', 'label', 'price' ) each
	// ...plus any settings an add-on keeps on the field
)

Reading options on an order

Each order item keeps a structured copy of its options in the hidden _optionline meta:

use Extendiment\Optionline\Orders;

foreach ( $order->get_items() as $item ) {
	foreach ( Orders::entries( $item ) as $entry ) {
		// $entry: id, label, type, value (a string, or a list of choice ids), display (as text),
		// unit (added to each item's price), once (added to the line once)
		error_log( $entry['label'] . ': ' . $entry['display'] );
	}
}

The readable copies ("Engraving" => "ANA (+$6.00)") are ordinary order item meta.

Field types

optionline_field_types

Register a type. Optionline's own five can't be replaced.

add_filter( 'optionline_field_types', function ( $types ) {
	$types['email'] = array(
		'label'       => __( 'Email', 'my-plugin' ),
		'choices'     => false, // customers pick from choices
		'multiple'    => false, // more than one choice
		'placeholder' => true,  // takes a placeholder
		'priced'      => true,  // a type without choices has a price of its own
	);
	return $types;
} );

optionline_field_html

Render a type on the product page. Extendiment\Optionline\Frontend::row() wraps your control in WooCommerce's form row with the label and description. The input's name must be optionline[{id}].

use Extendiment\Optionline\Frontend;

add_filter( 'optionline_field_html', function ( $html, $field, $value, $product ) {
	if ( 'email' !== $field['type'] ) {
		return $html;
	}
	$input = sprintf(
		'<input type="email" class="input-text" id="%s" name="%s" value="%s"%s>',
		esc_attr( Frontend::input_id( $field ) ),
		esc_attr( Frontend::input_name( $field ) ),
		esc_attr( (string) $value ),
		$field['required'] ? ' required' : ''
	);
	return Frontend::row( $field, $input, $product );
}, 10, 4 );

optionline_posted_value

Read a type's value from the add-to-cart form (or from an earlier order, for "Order again"). Gets the value as Optionline reads it, the field, what was submitted and where it came from (request, reorder or code).

add_filter( 'optionline_posted_value', function ( $value, $field, $raw, $source ) {
	return 'email' === $field['type'] ? sanitize_email( (string) $raw ) : $value;
}, 10, 4 );

Checking values

optionline_validate_field

Refuse a value with a message, or return '' to accept it. Runs for every field that applies, after Optionline's own checks (required, valid choices).

add_filter( 'optionline_validate_field', function ( $problem, $field, $value, $product ) {
	if ( '' === $problem && 'email' === $field['type'] && '' !== $value && ! is_email( $value ) ) {
		/* translators: %s: field label */
		return sprintf( __( '%s must be an email address.', 'my-plugin' ), $field['label'] );
	}
	return $problem;
}, 10, 4 );

optionline_field_applies

Whether a field applies to what the customer chose. A field that doesn't apply is skipped: not required, not stored and not charged. Gets the field, every field's value, all of the product's fields and the product.

// Only ask for a card message when "Handwritten card" is ticked.
add_filter( 'optionline_field_applies', function ( $applies, $field, $values ) {
	if ( 'note' === $field['id'] ) {
		return in_array( 'card', (array) ( $values['gift'] ?? array() ), true );
	}
	return $applies;
}, 10, 3 );

Prices

optionline_field_price

What a value adds, as entered (like product prices): unit is added to each item's price, once to the cart line once. Optionline adds the field's or the chosen choices' prices to unit.

// Engraving: $1 per letter.
add_filter( 'optionline_field_price', function ( $amount, $field, $value, $product ) {
	if ( 'engraving' === $field['id'] ) {
		$amount['unit'] = mb_strlen( str_replace( ' ', '', $value ) ) * 1.0;
	}
	return $amount;
}, 10, 4 );

Show the same rule on the product page with optionline_price_label (the text after a label), and in the live total by adding a price type in JavaScript: window.optionline.priceTypes.myType = ( price, field, value, context ) => … with optionline_field_config setting the field's priceType.

Fields on products

optionline_product_fields

The fields a product shows, in order: its own, plus any you add. Each is cleaned and checked like the product's own.

// A gift message on every product in the "Gifts" category.
add_filter( 'optionline_product_fields', function ( $fields, $product ) {
	if ( has_term( 'gifts', 'product_cat', $product->get_id() ) ) {
		$fields[] = array( 'id' => 'giftmsg', 'label' => 'Gift message', 'type' => 'textarea', 'price' => '' );
	}
	return $fields;
}, 10, 2 );

Hooks for add-ons

Hook What it's for
optionline_loaded Fires once Optionline has loaded (on plugins_loaded, only with WooCommerce active). OPTIONLINE_VERSION is defined from then on.
optionline_clean_field Clean and keep a setting of your own on a field ($clean, $raw). Empty settings are dropped; Optionline's own settings in the result are ignored.
optionline_handled_settings Return the names of the settings your code handles. A field that uses a setting nobody handles is held back from the shop (see below).
optionline_field_settings Print rows in a field's box ($field, $prefix). Use Admin\FieldEditor::row() and FieldEditor::name( $prefix, 'setting' ); a row with $types only shows for those types.
optionline_choice_headers, optionline_choice_cells Add columns to the choices table.
optionline_posted_field Read your settings from the posted field ($field, $raw, $stored; nonce and capability already checked). Set every setting you show, empty ones too.
optionline_product_types Product types that can have options (default simple and variable).
optionline_requires_choice Whether the shop's buttons send customers to the product page.
optionline_cart_entry An entry as it's stored on the cart line and the order item.
optionline_display_value A value as text in the cart, on orders and in emails.
optionline_order_item_meta The readable meta saved on the order item for an entry.
optionline_price_label The price after a field's label or a choice's name on the product page.
optionline_field_config, optionline_frontend_config What the product page's script gets.
optionline_show_totals Whether the product page shows the totals.
optionline_before_fields, optionline_after_fields Print something before or after the fields.
optionline_enqueue_scripts Enqueue your scripts with Optionline's on the product page.
optionline_product_tab, optionline_save_product Add to, and save, the product's Options tab.

Settings nobody handles

A field whose type isn't registered, or that has a setting (on the field or one of its choices) that no active plugin lists in optionline_handled_settings, is kept with all its settings but left off the product page and out of anything added to the cart from then on (lines already in a cart keep what they were added with). The field's box in the editor says why. This is what keeps a field set up with an add-on from being shown or charged the wrong way while the add-on is inactive.

The code documents each hook's arguments where it's applied.