2026-01-26 16:14:15 +01:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* Licensed Variable Product Class
|
|
|
|
|
*
|
|
|
|
|
* @package Jeremias\WcLicensedProduct\Product
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace Jeremias\WcLicensedProduct\Product;
|
|
|
|
|
|
|
|
|
|
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
|
|
|
|
use WC_Product_Variable;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Licensed Variable Product type extending WooCommerce Variable Product
|
|
|
|
|
*
|
|
|
|
|
* This allows selling license subscriptions with different durations
|
|
|
|
|
* (e.g., monthly, yearly, lifetime) as product variations.
|
|
|
|
|
*/
|
|
|
|
|
class LicensedVariableProduct extends WC_Product_Variable
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* Product type
|
|
|
|
|
*/
|
|
|
|
|
protected $product_type = 'licensed-variable';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Constructor
|
|
|
|
|
*/
|
|
|
|
|
public function __construct($product = 0)
|
|
|
|
|
{
|
|
|
|
|
parent::__construct($product);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get product type
|
|
|
|
|
*/
|
|
|
|
|
public function get_type(): string
|
|
|
|
|
{
|
|
|
|
|
return 'licensed-variable';
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 14:44:57 +01:00
|
|
|
/**
|
|
|
|
|
* Check if product is of a certain type
|
|
|
|
|
* Override to return true for 'variable' as well, so WooCommerce internal
|
|
|
|
|
* checks pass (many methods in WC_Product_Variable check is_type('variable'))
|
|
|
|
|
*/
|
|
|
|
|
public function is_type($type): bool
|
|
|
|
|
{
|
|
|
|
|
if (is_array($type)) {
|
|
|
|
|
return in_array($this->get_type(), $type, true) || in_array('variable', $type, true);
|
|
|
|
|
}
|
|
|
|
|
return $this->get_type() === $type || 'variable' === $type;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 16:14:15 +01:00
|
|
|
/**
|
|
|
|
|
* Licensed products are always virtual
|
|
|
|
|
*/
|
|
|
|
|
public function is_virtual(): bool
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-27 13:58:07 +01:00
|
|
|
* Licensed variable products are purchasable if the parent check passes
|
|
|
|
|
* Variable products don't have a direct price - their variations do
|
2026-01-26 16:14:15 +01:00
|
|
|
*/
|
|
|
|
|
public function is_purchasable(): bool
|
|
|
|
|
{
|
2026-01-27 13:58:07 +01:00
|
|
|
// Use the parent WC_Product_Variable logic
|
|
|
|
|
// which checks exists() and status, not price
|
|
|
|
|
return parent::is_purchasable();
|
2026-01-26 16:14:15 +01:00
|
|
|
}
|
|
|
|
|
|
2026-01-27 14:44:57 +01:00
|
|
|
/**
|
|
|
|
|
* Licensed products are always in stock (virtual, no inventory)
|
|
|
|
|
*/
|
|
|
|
|
public function is_in_stock(): bool
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get children (variations) for this product
|
|
|
|
|
* Override because WC_Product_Variable::get_children() checks is_type('variable')
|
|
|
|
|
* which fails for our 'licensed-variable' type
|
|
|
|
|
*/
|
|
|
|
|
public function get_children($context = 'view'): array
|
|
|
|
|
{
|
|
|
|
|
if (!$this->get_id()) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Query variations directly from database since WooCommerce's data store
|
|
|
|
|
// doesn't work properly with custom variable product types
|
|
|
|
|
global $wpdb;
|
|
|
|
|
$children = $wpdb->get_col($wpdb->prepare(
|
|
|
|
|
"SELECT ID FROM {$wpdb->posts}
|
|
|
|
|
WHERE post_parent = %d
|
|
|
|
|
AND post_type = 'product_variation'
|
|
|
|
|
AND post_status IN ('publish', 'private')
|
|
|
|
|
ORDER BY menu_order ASC, ID ASC",
|
|
|
|
|
$this->get_id()
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
$children = array_map('intval', $children);
|
|
|
|
|
|
|
|
|
|
if ('view' === $context) {
|
|
|
|
|
$children = apply_filters('woocommerce_get_children', $children, $this, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return is_array($children) ? $children : [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get variation attributes for this product
|
|
|
|
|
* Override because WC_Product_Variable uses data_store which doesn't work
|
|
|
|
|
* properly with custom variable product types
|
|
|
|
|
*/
|
|
|
|
|
public function get_variation_attributes(): array
|
|
|
|
|
{
|
|
|
|
|
$attributes = $this->get_attributes();
|
|
|
|
|
|
|
|
|
|
if (!$attributes || !is_array($attributes)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$variation_attributes = [];
|
|
|
|
|
|
|
|
|
|
foreach ($attributes as $attribute) {
|
|
|
|
|
// For WC_Product_Attribute objects
|
|
|
|
|
if ($attribute instanceof \WC_Product_Attribute) {
|
|
|
|
|
if ($attribute->get_variation()) {
|
|
|
|
|
$attribute_name = $attribute->get_name();
|
|
|
|
|
|
|
|
|
|
// For taxonomy attributes, get term slugs
|
|
|
|
|
if ($attribute->is_taxonomy()) {
|
|
|
|
|
$attribute_terms = wc_get_product_terms(
|
|
|
|
|
$this->get_id(),
|
|
|
|
|
$attribute_name,
|
|
|
|
|
['fields' => 'slugs']
|
|
|
|
|
);
|
|
|
|
|
$variation_attributes[$attribute_name] = $attribute_terms;
|
|
|
|
|
} else {
|
|
|
|
|
// For custom attributes, get options directly
|
|
|
|
|
$variation_attributes[$attribute_name] = $attribute->get_options();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// For array-based attributes (older format)
|
|
|
|
|
elseif (is_array($attribute) && !empty($attribute['is_variation'])) {
|
|
|
|
|
$attribute_name = $attribute['name'];
|
|
|
|
|
$values = isset($attribute['value']) ? explode('|', $attribute['value']) : [];
|
|
|
|
|
$variation_attributes[$attribute_name] = array_map('trim', $values);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $variation_attributes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get variation prices (regular, sale, and final prices)
|
|
|
|
|
* Override because WC_Product_Variable uses data_store which doesn't work
|
|
|
|
|
* properly with custom variable product types
|
|
|
|
|
*/
|
|
|
|
|
public function get_variation_prices($for_display = false): array
|
|
|
|
|
{
|
|
|
|
|
$children = $this->get_children();
|
|
|
|
|
|
|
|
|
|
if (empty($children)) {
|
|
|
|
|
return [
|
|
|
|
|
'price' => [],
|
|
|
|
|
'regular_price' => [],
|
|
|
|
|
'sale_price' => [],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$prices = [
|
|
|
|
|
'price' => [],
|
|
|
|
|
'regular_price' => [],
|
|
|
|
|
'sale_price' => [],
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
foreach ($children as $child_id) {
|
|
|
|
|
$variation = wc_get_product($child_id);
|
|
|
|
|
if ($variation) {
|
|
|
|
|
$price = $variation->get_price();
|
|
|
|
|
$regular_price = $variation->get_regular_price();
|
|
|
|
|
$sale_price = $variation->get_sale_price();
|
|
|
|
|
|
|
|
|
|
if ('' !== $price) {
|
|
|
|
|
$prices['price'][$child_id] = $price;
|
|
|
|
|
}
|
|
|
|
|
if ('' !== $regular_price) {
|
|
|
|
|
$prices['regular_price'][$child_id] = $regular_price;
|
|
|
|
|
}
|
|
|
|
|
if ('' !== $sale_price) {
|
|
|
|
|
$prices['sale_price'][$child_id] = $sale_price;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort prices
|
|
|
|
|
asort($prices['price']);
|
|
|
|
|
asort($prices['regular_price']);
|
|
|
|
|
asort($prices['sale_price']);
|
|
|
|
|
|
|
|
|
|
$this->prices_array = $prices;
|
|
|
|
|
|
|
|
|
|
return $this->prices_array;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get available variations for this product
|
|
|
|
|
* Override because WC_Product_Variable uses data_store which doesn't work
|
|
|
|
|
* properly with custom variable product types
|
|
|
|
|
*/
|
|
|
|
|
public function get_available_variations($return = 'array')
|
|
|
|
|
{
|
|
|
|
|
$children = $this->get_children();
|
|
|
|
|
$available_variations = [];
|
|
|
|
|
|
|
|
|
|
foreach ($children as $child_id) {
|
|
|
|
|
$variation = wc_get_product($child_id);
|
|
|
|
|
|
|
|
|
|
if (!$variation) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if variation should be available
|
|
|
|
|
if (!$variation->exists()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if purchasable (has price)
|
|
|
|
|
if (!$variation->is_purchasable()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build variation data
|
|
|
|
|
if ($return === 'array') {
|
|
|
|
|
$variationData = $this->get_available_variation($variation);
|
|
|
|
|
// Override availability_html to be empty for licensed products
|
|
|
|
|
$variationData['availability_html'] = '';
|
|
|
|
|
$available_variations[] = $variationData;
|
|
|
|
|
} else {
|
|
|
|
|
$available_variations[] = $variation;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($return === 'array') {
|
|
|
|
|
$available_variations = array_values(array_filter($available_variations));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $available_variations;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 16:14:15 +01:00
|
|
|
/**
|
|
|
|
|
* Get max activations for this product (parent default)
|
|
|
|
|
* Falls back to default settings if not set on product
|
|
|
|
|
*/
|
|
|
|
|
public function get_max_activations(): int
|
|
|
|
|
{
|
|
|
|
|
$value = $this->get_meta('_licensed_max_activations', true);
|
|
|
|
|
if ($value !== '' && $value !== null) {
|
|
|
|
|
return max(1, (int) $value);
|
|
|
|
|
}
|
|
|
|
|
return SettingsController::getDefaultMaxActivations();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if product has custom max activations set
|
|
|
|
|
*/
|
|
|
|
|
public function has_custom_max_activations(): bool
|
|
|
|
|
{
|
|
|
|
|
$value = $this->get_meta('_licensed_max_activations', true);
|
|
|
|
|
return $value !== '' && $value !== null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get validity days (parent default - variations override this)
|
|
|
|
|
* Falls back to default settings if not set on product
|
|
|
|
|
*/
|
|
|
|
|
public function get_validity_days(): ?int
|
|
|
|
|
{
|
|
|
|
|
$value = $this->get_meta('_licensed_validity_days', true);
|
|
|
|
|
if ($value !== '' && $value !== null) {
|
|
|
|
|
return (int) $value > 0 ? (int) $value : null;
|
|
|
|
|
}
|
|
|
|
|
return SettingsController::getDefaultValidityDays();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if product has custom validity days set
|
|
|
|
|
*/
|
|
|
|
|
public function has_custom_validity_days(): bool
|
|
|
|
|
{
|
|
|
|
|
$value = $this->get_meta('_licensed_validity_days', true);
|
|
|
|
|
return $value !== '' && $value !== null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if license should be bound to major version
|
|
|
|
|
* Falls back to default settings if not set on product
|
|
|
|
|
*/
|
|
|
|
|
public function is_bound_to_version(): bool
|
|
|
|
|
{
|
|
|
|
|
$value = $this->get_meta('_licensed_bind_to_version', true);
|
|
|
|
|
if ($value !== '' && $value !== null) {
|
|
|
|
|
return $value === 'yes';
|
|
|
|
|
}
|
|
|
|
|
return SettingsController::getDefaultBindToVersion();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if product has custom bind to version setting
|
|
|
|
|
*/
|
|
|
|
|
public function has_custom_bind_to_version(): bool
|
|
|
|
|
{
|
|
|
|
|
$value = $this->get_meta('_licensed_bind_to_version', true);
|
|
|
|
|
return $value !== '' && $value !== null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get current software version (derived from latest product version)
|
|
|
|
|
*/
|
|
|
|
|
public function get_current_version(): string
|
|
|
|
|
{
|
|
|
|
|
$versionManager = new VersionManager();
|
|
|
|
|
$latestVersion = $versionManager->getLatestVersion($this->get_id());
|
|
|
|
|
|
|
|
|
|
return $latestVersion ? $latestVersion->getVersion() : '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get major version number from version string
|
|
|
|
|
*/
|
|
|
|
|
public function get_major_version(): int
|
|
|
|
|
{
|
|
|
|
|
$versionManager = new VersionManager();
|
|
|
|
|
$latestVersion = $versionManager->getLatestVersion($this->get_id());
|
|
|
|
|
|
|
|
|
|
if ($latestVersion) {
|
|
|
|
|
return $latestVersion->getMajorVersion();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
}
|