You've already forked wc-tier-and-package-prices
Release version 1.0.1
- Add Twig template engine integration - Migrate all templates to Twig format - Add German (Switzerland, Informal) translation - Improve template organization and security - Add Composer dependency management - Create comprehensive changelog
This commit is contained in:
48
includes/class-wc-tpp-admin.php
Normal file
48
includes/class-wc-tpp-admin.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin settings and configuration
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class WC_TPP_Admin {
|
||||
|
||||
public function __construct() {
|
||||
add_action('admin_menu', array($this, 'add_admin_menu'));
|
||||
add_action('admin_init', array($this, 'register_settings'));
|
||||
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts'));
|
||||
}
|
||||
|
||||
public function add_admin_menu() {
|
||||
add_submenu_page(
|
||||
'woocommerce',
|
||||
__('Tier & Package Prices', 'wc-tier-package-prices'),
|
||||
__('Tier & Package Prices', 'wc-tier-package-prices'),
|
||||
'manage_woocommerce',
|
||||
'wc-tier-package-prices',
|
||||
array($this, 'settings_page')
|
||||
);
|
||||
}
|
||||
|
||||
public function register_settings() {
|
||||
register_setting('wc_tpp_settings', 'wc_tpp_enable_tier_pricing');
|
||||
register_setting('wc_tpp_settings', 'wc_tpp_enable_package_pricing');
|
||||
register_setting('wc_tpp_settings', 'wc_tpp_display_table');
|
||||
register_setting('wc_tpp_settings', 'wc_tpp_display_position');
|
||||
}
|
||||
|
||||
public function enqueue_admin_scripts($hook) {
|
||||
if ('woocommerce_page_wc-tier-package-prices' === $hook || 'post.php' === $hook || 'post-new.php' === $hook) {
|
||||
wp_enqueue_style('wc-tpp-admin', WC_TPP_PLUGIN_URL . 'assets/css/admin.css', array(), WC_TPP_VERSION);
|
||||
wp_enqueue_script('wc-tpp-admin', WC_TPP_PLUGIN_URL . 'assets/js/admin.js', array('jquery'), WC_TPP_VERSION, true);
|
||||
}
|
||||
}
|
||||
|
||||
public function settings_page() {
|
||||
WC_TPP_Template_Loader::get_instance()->display('admin/settings-page.twig');
|
||||
}
|
||||
}
|
||||
|
||||
new WC_TPP_Admin();
|
||||
94
includes/class-wc-tpp-cart.php
Normal file
94
includes/class-wc-tpp-cart.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
/**
|
||||
* Cart price calculation for tier and package pricing
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class WC_TPP_Cart {
|
||||
|
||||
public function __construct() {
|
||||
add_action('woocommerce_before_calculate_totals', array($this, 'apply_tier_package_pricing'), 10, 1);
|
||||
add_filter('woocommerce_cart_item_price', array($this, 'display_cart_item_price'), 10, 3);
|
||||
add_filter('woocommerce_cart_item_subtotal', array($this, 'display_cart_item_subtotal'), 10, 3);
|
||||
}
|
||||
|
||||
public function apply_tier_package_pricing($cart) {
|
||||
if (is_admin() && !defined('DOING_AJAX')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent infinite loops
|
||||
if (did_action('woocommerce_before_calculate_totals') >= 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if cart object is valid
|
||||
if (!$cart || !is_a($cart, 'WC_Cart')) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
|
||||
$product_id = $cart_item['product_id'];
|
||||
$quantity = $cart_item['quantity'];
|
||||
$product = $cart_item['data'];
|
||||
|
||||
// Validate product object
|
||||
if (!$product || !is_a($product, 'WC_Product')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for exact package match first
|
||||
$package_price = null;
|
||||
if (get_option('wc_tpp_enable_package_pricing') === 'yes') {
|
||||
$package_price = WC_TPP_Frontend::get_package_price($product_id, $quantity);
|
||||
}
|
||||
|
||||
if ($package_price !== null) {
|
||||
// Apply package pricing (total price divided by quantity)
|
||||
$unit_price = $package_price / $quantity;
|
||||
$product->set_price($unit_price);
|
||||
// Store pricing information in cart item for display
|
||||
WC()->cart->cart_contents[$cart_item_key]['wc_tpp_pricing_type'] = 'package';
|
||||
WC()->cart->cart_contents[$cart_item_key]['wc_tpp_total_price'] = $package_price;
|
||||
} else {
|
||||
// Apply tier pricing if no package match
|
||||
if (get_option('wc_tpp_enable_tier_pricing') === 'yes') {
|
||||
$tier_price = WC_TPP_Frontend::get_tier_price($product_id, $quantity);
|
||||
if ($tier_price !== null) {
|
||||
$product->set_price($tier_price);
|
||||
// Store pricing information in cart item for display
|
||||
WC()->cart->cart_contents[$cart_item_key]['wc_tpp_pricing_type'] = 'tier';
|
||||
WC()->cart->cart_contents[$cart_item_key]['wc_tpp_unit_price'] = $tier_price;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function display_cart_item_price($price, $cart_item, $cart_item_key) {
|
||||
if (isset($cart_item['wc_tpp_pricing_type'])) {
|
||||
if ($cart_item['wc_tpp_pricing_type'] === 'package') {
|
||||
$total_price = isset($cart_item['wc_tpp_total_price']) ? $cart_item['wc_tpp_total_price'] : $cart_item['line_total'];
|
||||
$unit_price = $total_price / $cart_item['quantity'];
|
||||
return wc_price($unit_price) . ' <small class="wc-tpp-notice">(' . __('Package price', 'wc-tier-package-prices') . ')</small>';
|
||||
} elseif ($cart_item['wc_tpp_pricing_type'] === 'tier') {
|
||||
$unit_price = isset($cart_item['wc_tpp_unit_price']) ? $cart_item['wc_tpp_unit_price'] : $cart_item['data']->get_price();
|
||||
return wc_price($unit_price) . ' <small class="wc-tpp-notice">(' . __('Volume discount', 'wc-tier-package-prices') . ')</small>';
|
||||
}
|
||||
}
|
||||
return $price;
|
||||
}
|
||||
|
||||
public function display_cart_item_subtotal($subtotal, $cart_item, $cart_item_key) {
|
||||
if (isset($cart_item['wc_tpp_pricing_type']) && $cart_item['wc_tpp_pricing_type'] === 'package') {
|
||||
$total_price = isset($cart_item['wc_tpp_total_price']) ? $cart_item['wc_tpp_total_price'] : $cart_item['line_total'];
|
||||
return wc_price($total_price);
|
||||
}
|
||||
return $subtotal;
|
||||
}
|
||||
}
|
||||
|
||||
new WC_TPP_Cart();
|
||||
109
includes/class-wc-tpp-frontend.php
Normal file
109
includes/class-wc-tpp-frontend.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
/**
|
||||
* Frontend display for tier and package pricing
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class WC_TPP_Frontend {
|
||||
|
||||
public function __construct() {
|
||||
add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts'));
|
||||
add_action('woocommerce_before_add_to_cart_button', array($this, 'display_pricing_table_before'), 20);
|
||||
add_action('woocommerce_after_add_to_cart_button', array($this, 'display_pricing_table_after'), 10);
|
||||
add_action('woocommerce_single_product_summary', array($this, 'display_pricing_table_after_price'), 15);
|
||||
}
|
||||
|
||||
public function enqueue_scripts() {
|
||||
if (is_product()) {
|
||||
wp_enqueue_style('wc-tpp-frontend', WC_TPP_PLUGIN_URL . 'assets/css/frontend.css', array(), WC_TPP_VERSION);
|
||||
wp_enqueue_script('wc-tpp-frontend', WC_TPP_PLUGIN_URL . 'assets/js/frontend.js', array('jquery'), WC_TPP_VERSION, true);
|
||||
|
||||
// Localize script with currency settings
|
||||
wp_localize_script('wc-tpp-frontend', 'wcTppData', array(
|
||||
'currency_symbol' => esc_js(get_woocommerce_currency_symbol()),
|
||||
'currency_position' => esc_js(get_option('woocommerce_currency_pos', 'left')),
|
||||
'price_decimals' => absint(wc_get_price_decimals()),
|
||||
'price_decimal_separator' => esc_js(wc_get_price_decimal_separator()),
|
||||
'price_thousand_separator' => esc_js(wc_get_price_thousand_separator())
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public function display_pricing_table_before() {
|
||||
if (get_option('wc_tpp_display_position') === 'before_add_to_cart') {
|
||||
$this->display_pricing_table();
|
||||
}
|
||||
}
|
||||
|
||||
public function display_pricing_table_after() {
|
||||
if (get_option('wc_tpp_display_position') === 'after_add_to_cart') {
|
||||
$this->display_pricing_table();
|
||||
}
|
||||
}
|
||||
|
||||
public function display_pricing_table_after_price() {
|
||||
if (get_option('wc_tpp_display_position') === 'after_price') {
|
||||
$this->display_pricing_table();
|
||||
}
|
||||
}
|
||||
|
||||
public function display_pricing_table() {
|
||||
global $product;
|
||||
|
||||
if (!$product || !is_a($product, 'WC_Product') || get_option('wc_tpp_display_table') !== 'yes') {
|
||||
return;
|
||||
}
|
||||
|
||||
$product_id = $product->get_id();
|
||||
$tiers = get_post_meta($product_id, '_wc_tpp_tiers', true);
|
||||
$packages = get_post_meta($product_id, '_wc_tpp_packages', true);
|
||||
|
||||
if (empty($tiers) && empty($packages)) {
|
||||
return;
|
||||
}
|
||||
|
||||
WC_TPP_Template_Loader::get_instance()->display('frontend/pricing-table.twig', array(
|
||||
'product' => $product,
|
||||
'tiers' => $tiers,
|
||||
'packages' => $packages
|
||||
));
|
||||
}
|
||||
|
||||
public static function get_tier_price($product_id, $quantity) {
|
||||
$tiers = get_post_meta($product_id, '_wc_tpp_tiers', true);
|
||||
|
||||
if (empty($tiers) || !is_array($tiers)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$applicable_price = null;
|
||||
foreach ($tiers as $tier) {
|
||||
if ($quantity >= $tier['min_qty']) {
|
||||
$applicable_price = $tier['price'];
|
||||
}
|
||||
}
|
||||
|
||||
return $applicable_price;
|
||||
}
|
||||
|
||||
public static function get_package_price($product_id, $quantity) {
|
||||
$packages = get_post_meta($product_id, '_wc_tpp_packages', true);
|
||||
|
||||
if (empty($packages) || !is_array($packages)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($packages as $package) {
|
||||
if ($quantity == $package['qty']) {
|
||||
return $package['price'];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
new WC_TPP_Frontend();
|
||||
157
includes/class-wc-tpp-product-meta.php
Normal file
157
includes/class-wc-tpp-product-meta.php
Normal file
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
/**
|
||||
* Product meta boxes for tier and package pricing
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class WC_TPP_Product_Meta {
|
||||
|
||||
public function __construct() {
|
||||
add_action('woocommerce_product_options_pricing', array($this, 'add_tier_pricing_fields'));
|
||||
add_action('woocommerce_product_options_pricing', array($this, 'add_package_pricing_fields'));
|
||||
add_action('woocommerce_process_product_meta', array($this, 'save_tier_package_fields'));
|
||||
}
|
||||
|
||||
public function add_tier_pricing_fields() {
|
||||
global $post;
|
||||
?>
|
||||
<div class="options_group wc-tpp-tier-pricing">
|
||||
<p class="form-field">
|
||||
<label><?php _e('Tier Pricing', 'wc-tier-package-prices'); ?></label>
|
||||
<span class="description"><?php _e('Set quantity-based pricing tiers. Customers get discounted prices when buying in larger quantities.', 'wc-tier-package-prices'); ?></span>
|
||||
</p>
|
||||
|
||||
<div class="wc-tpp-tiers-container">
|
||||
<?php
|
||||
$tiers = get_post_meta($post->ID, '_wc_tpp_tiers', true);
|
||||
if (!is_array($tiers)) {
|
||||
$tiers = array();
|
||||
}
|
||||
|
||||
foreach ($tiers as $index => $tier) {
|
||||
$this->render_tier_row($index, $tier);
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
<p class="form-field">
|
||||
<button type="button" class="button wc-tpp-add-tier"><?php _e('Add Tier', 'wc-tier-package-prices'); ?></button>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function add_package_pricing_fields() {
|
||||
global $post;
|
||||
?>
|
||||
<div class="options_group wc-tpp-package-pricing">
|
||||
<p class="form-field">
|
||||
<label><?php _e('Package Pricing', 'wc-tier-package-prices'); ?></label>
|
||||
<span class="description"><?php _e('Set fixed-price packages with specific quantities. For example: 10 pieces for $50, 25 pieces for $100.', 'wc-tier-package-prices'); ?></span>
|
||||
</p>
|
||||
|
||||
<div class="wc-tpp-packages-container">
|
||||
<?php
|
||||
$packages = get_post_meta($post->ID, '_wc_tpp_packages', true);
|
||||
if (!is_array($packages)) {
|
||||
$packages = array();
|
||||
}
|
||||
|
||||
foreach ($packages as $index => $package) {
|
||||
$this->render_package_row($index, $package);
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
<p class="form-field">
|
||||
<button type="button" class="button wc-tpp-add-package"><?php _e('Add Package', 'wc-tier-package-prices'); ?></button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script type="text/html" id="wc-tpp-tier-row-template">
|
||||
<?php $this->render_tier_row('{{INDEX}}', array('min_qty' => '', 'price' => '')); ?>
|
||||
</script>
|
||||
|
||||
<script type="text/html" id="wc-tpp-package-row-template">
|
||||
<?php $this->render_package_row('{{INDEX}}', array('qty' => '', 'price' => '', 'label' => '')); ?>
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
private function render_tier_row($index, $tier) {
|
||||
WC_TPP_Template_Loader::get_instance()->display('admin/tier-row.twig', array(
|
||||
'index' => $index,
|
||||
'tier' => $tier
|
||||
));
|
||||
}
|
||||
|
||||
private function render_package_row($index, $package) {
|
||||
WC_TPP_Template_Loader::get_instance()->display('admin/package-row.twig', array(
|
||||
'index' => $index,
|
||||
'package' => $package
|
||||
));
|
||||
}
|
||||
|
||||
public function save_tier_package_fields($post_id) {
|
||||
// Verify nonce for security
|
||||
if (!isset($_POST['woocommerce_meta_nonce']) || !wp_verify_nonce($_POST['woocommerce_meta_nonce'], 'woocommerce_save_data')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check user permissions
|
||||
if (!current_user_can('edit_post', $post_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid auto-save
|
||||
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save tier pricing
|
||||
if (isset($_POST['_wc_tpp_tiers'])) {
|
||||
$tiers = array();
|
||||
foreach ($_POST['_wc_tpp_tiers'] as $tier) {
|
||||
if (!empty($tier['min_qty']) && !empty($tier['price'])) {
|
||||
$tiers[] = array(
|
||||
'min_qty' => absint($tier['min_qty']),
|
||||
'price' => wc_format_decimal($tier['price'])
|
||||
);
|
||||
}
|
||||
}
|
||||
// Sort by minimum quantity
|
||||
usort($tiers, function($a, $b) {
|
||||
return $a['min_qty'] - $b['min_qty'];
|
||||
});
|
||||
update_post_meta($post_id, '_wc_tpp_tiers', $tiers);
|
||||
} else {
|
||||
delete_post_meta($post_id, '_wc_tpp_tiers');
|
||||
}
|
||||
|
||||
// Save package pricing
|
||||
if (isset($_POST['_wc_tpp_packages'])) {
|
||||
$packages = array();
|
||||
foreach ($_POST['_wc_tpp_packages'] as $package) {
|
||||
if (!empty($package['qty']) && !empty($package['price'])) {
|
||||
$packages[] = array(
|
||||
'qty' => absint($package['qty']),
|
||||
'price' => wc_format_decimal($package['price']),
|
||||
'label' => sanitize_text_field($package['label'])
|
||||
);
|
||||
}
|
||||
}
|
||||
// Sort by quantity
|
||||
usort($packages, function($a, $b) {
|
||||
return $a['qty'] - $b['qty'];
|
||||
});
|
||||
update_post_meta($post_id, '_wc_tpp_packages', $packages);
|
||||
} else {
|
||||
delete_post_meta($post_id, '_wc_tpp_packages');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new WC_TPP_Product_Meta();
|
||||
103
includes/class-wc-tpp-template-loader.php
Normal file
103
includes/class-wc-tpp-template-loader.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
/**
|
||||
* Twig Template Loader for WC Tier Package Prices
|
||||
*
|
||||
* @package WC_Tier_Package_Prices
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
use Twig\Environment;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class WC_TPP_Template_Loader {
|
||||
|
||||
private static $instance = null;
|
||||
private $twig;
|
||||
|
||||
public static function get_instance() {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
$this->init_twig();
|
||||
}
|
||||
|
||||
private function init_twig() {
|
||||
$loader = new FilesystemLoader(WC_TPP_PLUGIN_DIR . 'templates');
|
||||
|
||||
$this->twig = new Environment($loader, array(
|
||||
'cache' => WP_DEBUG ? false : WC_TPP_PLUGIN_DIR . 'templates/cache',
|
||||
'auto_reload' => true,
|
||||
'autoescape' => 'html',
|
||||
));
|
||||
|
||||
// Add WordPress translation filter
|
||||
$this->twig->addFilter(new TwigFilter('__', function ($string, $domain = 'wc-tier-package-prices') {
|
||||
return __($string, $domain);
|
||||
}));
|
||||
|
||||
$this->twig->addFilter(new TwigFilter('_e', function ($string, $domain = 'wc-tier-package-prices') {
|
||||
_e($string, $domain);
|
||||
}));
|
||||
|
||||
$this->twig->addFilter(new TwigFilter('esc_html', 'esc_html'));
|
||||
$this->twig->addFilter(new TwigFilter('esc_attr', 'esc_attr'));
|
||||
$this->twig->addFilter(new TwigFilter('esc_url', 'esc_url'));
|
||||
|
||||
// Add WordPress functions
|
||||
$this->twig->addFunction(new TwigFunction('get_option', 'get_option'));
|
||||
$this->twig->addFunction(new TwigFunction('checked', function($checked, $current = true, $echo = false) {
|
||||
return checked($checked, $current, $echo);
|
||||
}));
|
||||
$this->twig->addFunction(new TwigFunction('selected', function($selected, $current = true, $echo = false) {
|
||||
return selected($selected, $current, $echo);
|
||||
}));
|
||||
$this->twig->addFunction(new TwigFunction('settings_fields', function($option_group) {
|
||||
settings_fields($option_group);
|
||||
}));
|
||||
$this->twig->addFunction(new TwigFunction('submit_button', function($text = null) {
|
||||
submit_button($text);
|
||||
}));
|
||||
$this->twig->addFunction(new TwigFunction('get_admin_page_title', 'get_admin_page_title'));
|
||||
$this->twig->addFunction(new TwigFunction('wc_price', 'wc_price'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a Twig template
|
||||
*
|
||||
* @param string $template Template file name (e.g., 'admin/settings-page.twig')
|
||||
* @param array $context Variables to pass to the template
|
||||
* @return string Rendered template
|
||||
*/
|
||||
public function render($template, $context = array()) {
|
||||
try {
|
||||
return $this->twig->render($template, $context);
|
||||
} catch (Exception $e) {
|
||||
if (WP_DEBUG) {
|
||||
return sprintf(
|
||||
'<div class="error"><p>Twig Error: %s</p></div>',
|
||||
esc_html($e->getMessage())
|
||||
);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a Twig template
|
||||
*
|
||||
* @param string $template Template file name
|
||||
* @param array $context Variables to pass to the template
|
||||
*/
|
||||
public function display($template, $context = array()) {
|
||||
echo $this->render($template, $context);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user