Skip to content

Product Variation Model ​

DB Table Name{wp_db_prefix}_fct_product_variations
SchemaCheck Schema
Source Filefluent-cart/app/Models/ProductVariation.php
Name SpaceFluentCart\App\Models
ClassFluentCart\App\Models\ProductVariation

Traits ​

TraitDescription
CanSearchAdds search scope capabilities
CanUpdateBatchAdds batch update capabilities

Attributes ​

AttributeData TypeComment
idIntegerPrimary Key
post_idIntegerReference to WordPress post (product)
media_idIntegerReference to media
serial_indexIntegerSerial index for ordering
sold_individuallyIntegerWhether sold individually (0 or 1)
variation_titleStringVariation title
variation_identifierStringVariation identifier
skuStringStock keeping unit
manage_stockStringWhether to manage stock
payment_typeStringPayment type (onetime, subscription)
stock_statusStringStock status
backordersIntegerBackorder quantity
total_stockIntegerTotal stock quantity
availableIntegerAvailable quantity
committedIntegerCommitted quantity
on_holdIntegerOn hold quantity
fulfillment_typeStringFulfillment type (physical, digital)
item_statusStringItem status (active, inactive)
manage_costStringWhether to manage cost
item_priceDecimalItem price (stored in cents, cast to double)
item_costDecimalItem cost (stored in cents, cast to double)
compare_priceDecimalCompare price (stored in cents, cast to double)
other_infoArrayAdditional variation information (JSON, cast to array)
downloadableStringWhether downloadable
shipping_classStringShipping class ID
created_atDate TimeCreation timestamp
updated_atDate TimeLast update timestamp

Casts ​

The following attributes are automatically cast when accessed:

AttributeCast Type
post_idinteger
media_idinteger
item_costdouble
item_pricedouble
compare_pricedouble
backordersinteger
total_stockinteger
availableinteger
committedinteger
on_holdinteger
sold_individuallyinteger
serial_indexinteger
other_infoarray

Appends ​

The following computed attributes are appended to every model instance:

Appended AttributeDescription
thumbnailAlways appended via $appends. Returns the first thumbnail URL from the media relation, or null.
formatted_totalAppended at runtime inside booted() on every retrieved event. Returns Helper::toDecimal($this->item_price).

Custom other_info Accessor ​

The getOtherInfoAttribute accessor decodes the JSON value and, when payment_type is subscription, injects sensible defaults for subscription fields that may not yet be stored:

Injected KeyDefault Value
payment_type'subscription'
installment'yes' only if value is 'yes' AND Pro is active; otherwise 'no'
repeat_interval'yearly'
times0
trial_days0
manage_setup_fee'no'

This means reading $variation->other_info on a subscription variation always returns these keys even if they were not explicitly saved.

Cascade Deletes ​

When a ProductVariation is deleted, the boot() method fires the following cleanup:

  1. Media -- calls \FluentCart\Api\Meta::deleteVariationMedia($model->id) to remove associated media metadata.
  2. Attribute Map -- calls $model->attrMap()->delete() to remove all related AttributeRelation records.

Usage ​

Please check Model Basic for Common methods.

Accessing Attributes ​

php
$productVariation = FluentCart\App\Models\ProductVariation::find(1);

$productVariation->id; // returns id
$productVariation->post_id; // returns post ID
$productVariation->item_price; // returns item price (cast to double)
$productVariation->stock_status; // returns stock status
$productVariation->sku; // returns SKU
$productVariation->thumbnail; // returns thumbnail URL or null (appended attribute)
$productVariation->formatted_total; // returns formatted price string (appended on retrieval)

Scopes ​

This model has the following scopes that you can use

getWithShippingClass() ​

Get variations with their shipping class information. This scope executes the query, then looks up ShippingMethod records whose IDs match the other_info.shipping_class value on each variation, and attaches the matching method as a shipping_method dynamic attribute.

  • Parameters
    • none
  • Returns the query result (executes $query->get() internally)

Usage: ​

php
// Get variations with shipping class data
$variations = FluentCart\App\Models\ProductVariation::getWithShippingClass();

foreach ($variations as $variation) {
    if (isset($variation->shipping_method)) {
        echo $variation->shipping_method->title;
    }
}

Relations ​

attrMap / attrRelations ​

Both are hasMany to AttributeRelation on object_id — the variation's attribute term links. attrMap is also what the model's deleting hook clears: deleting a variation removes its attribute relations and its variation media.

php
$terms = $variation->attrRelations;

This model has the following relationships that you can use

product ​

Access the associated product (WordPress post). BelongsTo via post_id -> Product.ID.

  • return FluentCart\App\Models\Product Model

Example: ​

php
// Accessing Product
$product = $productVariation->product;

// For Filtering by product relationship
$productVariations = FluentCart\App\Models\ProductVariation::whereHas('product', function($query) {
    $query->where('post_status', 'publish');
})->get();

shippingClass ​

Access the associated shipping class. BelongsTo via shipping_class -> ShippingClass.id.

  • return FluentCart\App\Models\ShippingClass Model

Example: ​

php
// Accessing Shipping Class
$shippingClass = $productVariation->shippingClass;

product_detail ​

Access the associated product detail. BelongsTo via post_id -> ProductDetail.post_id.

  • return FluentCart\App\Models\ProductDetail Model

Example: ​

php
// Accessing Product Detail
$productDetail = $productVariation->product_detail;

media ​

Access the associated product thumbnail media. HasOne to ProductMeta via object_id -> id, filtered to meta_key = 'product_thumbnail'. Only selects id, object_id, meta_value.

  • return FluentCart\App\Models\ProductMeta Model (single record)

Example: ​

php
// Accessing Media
$media = $productVariation->media;

// The thumbnail appended attribute reads from this relation:
$url = $productVariation->thumbnail; // shortcut

product_downloads ​

Access all product downloads associated with this variation's product. HasMany to ProductDownload via post_id -> post_id, filtered to rows where product_variation_id contains this variation's ID, or is NULL, or is '[]'.

  • return FluentCart\App\Models\ProductDownload Model Collection

Example: ​

php
// Accessing Product Downloads
$downloads = $productVariation->product_downloads;

order_items ​

Access all order items for this variation. HasMany to OrderItem via object_id -> id.

  • return FluentCart\App\Models\OrderItem Model Collection

Example: ​

php
// Accessing Order Items
$orderItems = $productVariation->order_items;

downloadable_files ​

Access all downloadable files directly linked to this variation. HasMany to ProductDownload via product_variation_id -> id.

  • return FluentCart\App\Models\ProductDownload Model Collection

Example: ​

php
// Accessing Downloadable Files
$downloadableFiles = $productVariation->downloadable_files;

upgrade_paths ​

Access all upgrade path meta entries for this variation. HasMany to Meta via object_id -> id, filtered by object_type = PlanUpgradeService::$metaType and meta_key = PlanUpgradeService::$metaKey.

  • return FluentCart\App\Models\Meta Model Collection

Example: ​

php
// Accessing Upgrade Paths
$upgradePaths = $productVariation->upgrade_paths;

attrMap ​

Access all attribute relation mappings for this variation. HasMany to AttributeRelation via object_id -> id.

Note: These records are cascade-deleted when the variation is deleted.

  • return FluentCart\App\Models\AttributeRelation Model Collection

Example: ​

php
// Accessing Attribute Relations
$attrMap = $productVariation->attrMap;

bundleChildren ​

Access the child variations of a bundle product. Uses a custom BundleChildrenRelation that reads child IDs from the other_info JSON column (key bundle_child_ids) and loads the corresponding ProductVariation records. Supports eager loading.

  • return FluentCart\App\Models\ProductVariation Model Collection (via BundleChildrenRelation)

Example: ​

php
// Accessing Bundle Children
$children = $productVariation->bundleChildren;

// Eager loading bundle children
$variations = FluentCart\App\Models\ProductVariation::with('bundleChildren')->where('post_id', $postId)->get();

foreach ($variations as $variation) {
    foreach ($variation->bundleChildren as $child) {
        echo $child->variation_title;
    }
}

Methods ​

isStock($bundleChildren = null) ​

Whether the variation is in stock. For a bundle, pass the already-loaded children so the check does not re-query them per variation.

  • Parameters
    • $bundleChildren - array|null (optional) - pre-loaded bundle children
  • Returns bool

loadBundleChildren($variations) (static) ​

Resolve the bundle children for a set of variations in one pass, so isStock() can then be called per variation without an N+1.

  • Parameters
    • $variations - the variations to resolve children for

getVariationLabel() ​

The variation's label, built from its attribute terms.

  • Returns string

getDisplayTitle() ​

The title shown to customers for this variation.

  • Returns string

Along with Global Model methods, this model has few helper methods.

getFormattedTotalAttribute() ​

Get formatted total price (accessor). Automatically appended on every retrieved event via the booted() method.

  • Parameters
    • none
  • Returns string -- the decimal-formatted item price

Usage ​

php
$formattedTotal = $productVariation->formatted_total; // Returns formatted price string

getThumbnailAttribute() ​

Get thumbnail URL (accessor). Reads from the media relation. Returns the url key of the first element in media->meta_value, or null if no media is set.

  • Parameters
    • none
  • Returns string|null

Usage ​

php
$thumbnail = $productVariation->thumbnail; // Returns thumbnail URL or null

canPurchase($quantity = 1) ​

Check if the variation can be purchased. Validates:

  1. item_status must be active and parent product must be publish or private.
  2. Subscription variations cannot have $quantity > 1.
  3. A related product_detail must exist.
  4. If stock management module is active and both productDetail->manage_stock and $this->manage_stock are truthy, $quantity must not exceed $this->available.
  5. Bundle products require Pro to be active.
  6. Applies the fluent_cart/variation/can_purchase_bundle filter for additional bundle checks.
  • Parameters
    • $quantity - integer (default: 1)
  • Returns true on success, or \WP_Error with an error code on failure

Error codes: unpublished, invalid_subscription_quantity, insufficient_stock, invalid_bundle_product

Usage ​

php
$canPurchase = $productVariation->canPurchase(2);
if (is_wp_error($canPurchase)) {
    echo "Error: " . $canPurchase->get_error_message();
} else {
    echo "Available for purchase";
}

getSubscriptionTermsText($withComparePrice = false) ​

Get human-readable subscription terms text. Returns an empty string for non-subscription variations. Reads trial_days, repeat_interval, times, signup_fee, signup_fee_name from other_info and delegates to Helper::getSubscriptionTermText().

  • Parameters
    • $withComparePrice - boolean (default: false). When true and compare_price > item_price, the compare price is included in the formatted output.
  • Returns string

Usage ​

php
$termsText = $productVariation->getSubscriptionTermsText(true);
echo "Subscription Terms: " . $termsText;

getPurchaseUrl() ​

Get the instant checkout purchase URL for this variation.

  • Parameters
    • none
  • Returns string -- URL in the format site_url('?fluent-cart=instant_checkout&item_id={id}&quantity=1')

Usage ​

php
$purchaseUrl = $productVariation->getPurchaseUrl();
echo "Purchase URL: " . $purchaseUrl;

soldIndividually() ​

Check whether this variation's parent product is sold individually. Delegates to $this->product->soldIndividually().

  • Parameters
    • none
  • Returns bool -- false if no product is loaded

Usage ​

php
if ($productVariation->soldIndividually()) {
    echo "This product can only be purchased one at a time.";
}

isStock() ​

Check whether this variation is currently in stock. The logic handles both regular and bundle products:

  1. Returns false if item_status is not active.
  2. If manage_stock is disabled:
    • For bundle products, delegates to isBundleChildrenInStock().
    • For regular products, returns true when stock_status equals Helper::IN_STOCK.
  3. If manage_stock is enabled, checks available > 0 AND stock_status === Helper::IN_STOCK.
  4. For bundle products with stock management enabled, the parent must be in stock AND all children must pass isBundleChildrenInStock().
  • Parameters
    • none
  • Returns bool

Usage ​

php
$variation = FluentCart\App\Models\ProductVariation::find(1);
if ($variation->isStock()) {
    echo "In stock";
} else {
    echo "Out of stock";
}

isBundleChildrenInStock() (protected) ​

Check if all bundle children are in stock. Reads bundle_child_ids from other_info, loads those variations, and verifies each child is active and (if manage_stock is enabled) has available > 0 with stock_status === Helper::IN_STOCK. Returns true if there are no bundle children.

  • Visibility: protected
  • Parameters
    • none
  • Returns bool

Hooks / Filters ​

HookTypeLocationDescription
fluent_cart/variation/can_purchase_bundleFiltercanPurchase()Allows external code to block or allow bundle purchases. Receives null and an array with variation and quantity. Return \WP_Error to block, false for out-of-stock, or null/true to allow.

Usage Examples ​

Get Product Variations ​

php
$productVariation = FluentCart\App\Models\ProductVariation::find(1);
echo "Price: " . $productVariation->formatted_total;
echo "Stock: " . $productVariation->available;
echo "Status: " . $productVariation->item_status;
echo "SKU: " . $productVariation->sku;

Get Variations with Shipping Class ​

php
$variations = FluentCart\App\Models\ProductVariation::getWithShippingClass();
foreach ($variations as $variation) {
    echo "Variation: " . $variation->variation_title;
    if (isset($variation->shipping_method)) {
        echo "Shipping Method: " . $variation->shipping_method->title;
    }
}

Check Purchase Availability ​

php
$variation = FluentCart\App\Models\ProductVariation::find(1);
$canPurchase = $variation->canPurchase(1);

if (is_wp_error($canPurchase)) {
    echo "Cannot purchase: " . $canPurchase->get_error_message();
} else {
    echo "Available for purchase";
}

Check Stock Status (Including Bundles) ​

php
$variation = FluentCart\App\Models\ProductVariation::find(1);
if ($variation->isStock()) {
    echo "Variation is in stock";
} else {
    echo "Variation is out of stock";
}

Get Subscription Terms ​

php
$variation = FluentCart\App\Models\ProductVariation::find(1);
if ($variation->payment_type === 'subscription') {
    $terms = $variation->getSubscriptionTermsText(true);
    echo "Subscription: " . $terms;
}

Get Downloadable Files ​

php
$variation = FluentCart\App\Models\ProductVariation::find(1);
$downloads = $variation->downloadable_files;

foreach ($downloads as $download) {
    echo "Download: " . $download->title;
}

Work with Bundle Children ​

php
$variation = FluentCart\App\Models\ProductVariation::find(1);
$children = $variation->bundleChildren;

foreach ($children as $child) {
    echo "Child: " . $child->variation_title . " - Price: " . $child->formatted_total;
}

FluentCart developer documentation