Skip to content
Snippets Groups Projects
Commit 6225f094 authored by Reinhold Kainhofer's avatar Reinhold Kainhofer
Browse files

First steps at factoring out the shipping cost calculation to a framework...

First steps at factoring out the shipping cost calculation to a framework class/library; Joomla/VM plugin seems to work again
parent 44a43124
No related branches found
No related tags found
No related merge requests found
RulesShippingFramework:
=======================
API functions:
~~~~~~~~~~~~~~
-) setup() // Used in the plugin's constructor
-) registerCallback($callback, $func)
-) parseRuleSyntax($rulestring, $countries, $tax)
-) checkConditions ($cart, $method, $cart_prices)
-) getCosts($cart, $method, $cart_prices, $variables=null) // The central function to calculate the shipping cost
-) getRuleName($methodid)
-) getRuleVariables($methodid)
-) filterProducts($products, $conditions) // Used in the Rule's evaluateScoping
E-commerce specific functions (to be overridden in child classes!):
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-) printWarning($message)
-) __($string)
-) getCustomFunctions()
-) addPluginCartValues($cart, $products, $method, $cart_prices, &$values)
-) getOrderArticles($cart, $products)
-) getOrderProducts ($cart, $products)
-) getOrderDimensions ($cart, $products, $length_dimension)
-) getOrderWeights ($cart, $products, $weight_unit)
-) getOrderListProperties ($cart, $products)
-) getOrderAddress ($cart)
-) getOrderPrices ($cart, $products, $cart_prices)
-) filterProducts($products, $conditions) // Used in the Rule's evaluateScoping
Callback functions:
~~~~~~~~~~~~~~~~~~~
-) translate($message)
-) initRule($framework, $rulestring, $countries, $tax)
-) addCustomCartValues ($cart, $products, $cart_prices, &$values)
<?php
defined ('_JEXEC') or die('Restricted access');
/**
* Shipment plugin for general, rules-based shipments, like regular postal services with complex shipping cost structures
*
* @package VirtueMart
* @subpackage Plugins - shipment
* @copyright Copyright (C) 2004-2012 VirtueMart Team - All rights reserved.
* @copyright Copyright (C) 2013 Reinhold Kainhofer, reinhold@kainhofer.com
* @license http://www.gnu.org/copyleft/gpl.html GNU/GPL, see LICENSE.txt
* VirtueMart is free software. This version may have been modified pursuant
* to the GNU General Public License, and as distributed it includes or
* is derivative of works licensed under the GNU General Public License or
* other free or open source software licenses.
*
* @author Reinhold Kainhofer, based on the weight_countries shipping plugin by Valerie Isaksen
*
*/
// Only declare the class once...
if (class_exists ('RulesShippingFramework')) {
return;
}
function print_array($obj) {
$res = "";
if (is_array($obj)) {
$res .= "array(";
$sep = "";
foreach ($obj as $e) {
$res .= $sep . print_array($e);
$sep = ", ";
}
$res .= ")";
} elseif (is_string($obj)) {
$res .= "\"$obj\"";
} else {
$res .= (string)$obj;
}
return $res;
}
function is_equal($a, $b) {
if (is_array($a) && is_array($b)) {
return !array_diff($a, $b) && !array_diff($b, $a);
} elseif (is_string($a) && is_string($b)) {
return strcmp($a,$b) == 0;
} else {
return $a == $b;
}
}
class RulesShippingFramework {
static $_version = "0.1";
protected $_callbacks = array();
// Store the parsed and possibly evaluated rules for each method (method ID is used as key)
protected $rules = array();
protected $match = array();
protected $last_cartvals = array();
protected $last_rulename = '';
var $custom_functions = array ();
function __construct() {
// $this->registerCallback('initRule', array($this, 'createMethodRule'));
// $this->registerCallback('addCustomCartValues', array($this, 'addCustomCartValues'));
}
/* Callback handling */
/**
* Register a callback for one of the known callback hooks.
* Valid callbacks are (together with their arguments):
* - translate($string)
* @param string $callback
* The name of the callback hook (string)
* @param function $func
* The function (usually a member of the plugin object) for the callback
* @return none
*/
public function registerCallback($callback, $func) {
$this->callbacks[$callback] = $func;
}
public function __($string) {
if (isset($this->callbacks["translate"])) {
return $this->callbacks["translate"]($string);
} else {
return $string;
}
}
function getCustomFunctions() {
// Let other plugins add custom functions!
// This function is expected to return an array of the form:
// array ('functionname1' => 'function-to-be-called',
// 'functionname2' => array($classobject, 'memberfunc')),
// ...);
return array ();
}
/**
* Function printWarning: This needs to be overwritten in derived classes!
*/
public function printWarning($message) {
// TODO!
}
function setup() {
$custfuncdefs = $this->getCustomFunctions();
// Loop through the return values of all plugins:
foreach ($custfuncdefs as $custfuncs) {
if (empty($custfuncs))
continue;
if (!is_array($custfuncs)) {
$this->printWarning(JText::sprintf('VMSHIPMENT_RULES_CUSTOMFUNCTIONS_NOARRAY'));
}
// Now loop through all custom function definitions of this plugin
// If a function was registered before, print a warning and use the first definition
foreach ($custfuncs as $fname => $func) {
if (isset($this->custom_functions[$fname])) {
$this->printWarning(JText::sprintf('VMSHIPMENT_RULES_CUSTOMFUNCTIONS_ALREADY_DEFINED', $fname));
} else {
vmDebug("Defining custom function $fname");
$this->custom_functions[strtolower($fname)] = $func;
}
}
}
}
/**
* Functions to calculate the cart variables:
* - getOrderArticles($cart, $products)
* - getOrderProducts
* - getOrderDimensions
*/
/** Functions to calculate all the different variables for the given cart and given (sub)set of products in the cart */
protected function getOrderArticles ($cart, $products) {
return 0;
}
protected function getOrderProducts ($cart, $products) {
return count($products);
}
protected function getOrderDimensions ($cart, $products, $length_dimension) {
return array();
}
protected function getOrderWeights ($cart, $products, $weight_unit) {
return array();
}
protected function getOrderListProperties ($cart, $products) {
return array();
}
protected function getOrderAddress ($cart) {
return array();
}
protected function getOrderPrices ($cart, $products, $cart_prices) {
return array();
}
/** Allow child classes to add additional variables for the rules or modify existing one
*/
protected function addCustomCartValues ($cart, $products, $cart_prices, &$values) {
if (isset($this->callbacks['addCustomCartValues'])) {
return $this->callbacks['addCustomCartValues']($cart, $products, $cart_prices, $values);
}
}
protected function addPluginCartValues($cart, $products, $method, $cart_prices, &$values) {
}
public function getCartValues ($cart, $products, $method, $cart_prices) {
$cartvals = array_merge (
array(
'articles'=>$this->getOrderArticles($cart, $products),
'products'=>$this->getOrderProducts($cart, $products),
),
// Add the prices, optionally calculated from the products subset of the cart
$this->getOrderPrices ($cart, $products, $cart_prices),
// Add 'skus', 'categories', 'vendors' variables:
$this->getOrderListProperties ($cart, $products),
// Add country / state variables:
$this->getOrderAddress ($cart),
// Add Total/Min/Max weight and dimension variables:
$this->getOrderWeights ($cart, $products, $method->weight_unit),
$this->getOrderDimensions ($cart, $products, $method->length_unit)
);
// Let child classes update the $cartvals array, or add new variables
$this->addCustomCartValues($cart, $products, $cart_prices, $cartvals);
// Let custom plugins update the $cartvals array or add new variables
$this->addPluginCartValues($cart, $products, $method, $cart_prices, $cartvals);
return $cartvals;
}
/** This function evaluates all rules, one after the other until it finds a matching rule that
* defines shipping costs (or uses NoShipping). If a modifier or definition is encountered,
* its effect is stored, but the loop continues */
protected function evaluateMethodRules ($cart, $method, $cart_prices) {
$id = $method->virtuemart_shipmentmethod_id; // TODO: Generalize to other ecommerce systems!
// $method->match will cache the matched rule and the modifiers
if (isset($this->match[$id])) {
return $this->match[$id];
} else {
// Evaluate all rules and find the matching ones (including modifiers and definitions!)
$cartvals = $this->getCartValues ($cart, $cart->products, $method, $cart_prices);
$result = array("rule"=>Null, "rule_name"=>"", "modifiers_add"=>array(), "modifiers_multiply"=>array(), "cartvals"=>$cartvals);
// Pass a callback function to the rules to obtain the cartvals for a subset of the products
$this_class = $this;
$cartvals_callback = function ($products) use ($this_class, $cart, $method, $cart_prices) {
return $this_class->getCartValues ($cart, $products, $method, NULL);
};
foreach ($this->rules[$id] as $r) {
if ($r->matches($cartvals, $cart->products, $cartvals_callback)) {
$rtype = $r->getType();
switch ($rtype) {
case 'shipping':
case 'shippingwithtax':
case 'noshipping':
$result["rule"] = $r;
$result["rule_name"] = $r->getRuleName();
break;
case 'modifiers_add':
case 'modifiers_multiply':
$result[$rtype][] = $r;
break;
case 'definition': // A definition updates the $cartvals, but has no other effects
$cartvals[strtolower($r->getRuleName())] = $r->getValue();
break;
default:
$this->helper->printWarning(JText::sprintf('VMSHIPMENT_RULES_UNKNOWN_TYPE', $r->getType(), $r->rulestring));
break;
}
}
if (!is_null($result["rule"])) {
$this->match[$id] = $result;
return $result; // <- This also breaks out of the foreach loop!
}
}
}
// None of the rules matched, so return NULL, but keep the evaluated results;
$this->match[$id] = $result;
return NULL;
}
/**
* @param \VirtueMartCart $cart
* @param int $method
* @param array $cart_prices
* @return bool
*/
public function checkConditions ($cart, $method, $cart_prices) {
if (!isset($this->rules[$method->virtuemart_shipmentmethod_id]))
$this->parseMethodRules($method);
$match = $this->evaluateMethodRules ($cart, $method, $cart_prices);
if ($match && !is_null ($match['rule'])) {
$method->rule_name = $match["rule_name"];
// If NoShipping is set, this method should NOT offer any shipping at all, so return FALSE, otherwise TRUE
// If the rule has a name, print it as warning (otherwise don't print anything)
if ($match['rule']->isNoShipping()) {
if (!empty($method->rule_name))
$this->helper->pringWarning(JText::sprintf('VMSHIPMENT_RULES_NOSHIPPING_MESSAGE', $method->rule_name));
vmdebug('checkConditions '.$method->shipment_name.' indicates NoShipping for this method, specified by rule "'.$method->rule_name.'" ('.$match['rule']->rulestring.').');
return FALSE;
} else {
return TRUE;
}
}
vmdebug('checkConditions '.$method->shipment_name.' does not fulfill all conditions, no rule matches');
return FALSE;
}
/**
* @param VirtueMartCart $cart
* @param $method
* @param $cart_prices
* @return int
*/
function getCosts (VirtueMartCart $cart, $method, $cart_prices) {
if (!isset($this->rules[$method->virtuemart_shipmentmethod_id]))
$this->parseMethodRules($method);
$match = $this->evaluateMethodRules ($cart, $method, $cart_prices);
if ($match) {
$r = $match["rule"];
vmdebug('Rule ' . $match["rule_name"] . ' ('.$r->rulestring.') matched.');
$method->tax_id = $r->tax_id;
// TODO: Shall we include the name of the modifiers, too?
$method->rule_name = $match["rule_name"];
// Final shipping costs are calculated as:
// Shipping*ExtraShippingMultiplier + ExtraShippingCharge
// with possibly multiple modifiers
$method->cost = $r->getShippingCosts();
foreach ($match['modifiers_multiply'] as $modifier) {
$method->cost *= $modifier->getValue();
}
foreach ($match['modifiers_add'] as $modifier) {
$method->cost += $modifier->getValue();
}
$method->includes_tax = $r->includes_tax;
return $method->cost;
}
vmdebug('getCosts '.$method->name.' does not return shipping costs');
return 0;
}
public function getRuleName($methodid) {
if (isset($this->match[$methodid])) {
return $this->match[$methodid]["rule_name"];
} else {
return '';
}
}
public function getRuleVariables($methodid) {
if (isset($this->match[$methodid])) {
return $this->match[$methodid]["cartvals"];
} else {
return array();
}
}
protected function createMethodRule ($r, $countries, $tax) {
if (isset($this->callbacks['initRule'])) {
return $this->callbacks['initRule']($this, $r, $countries, $tax);
} else {
return new ShippingRule($this, $r, $countries, $tax);
}
}
// Parse the rule and append all rules to the rule set of the current shipment method (country/tax are already included in the rule itself!)
private function parseMethodRule ($rulestring, $countries, $tax, &$method) {
foreach ($this->parseRuleSyntax($rulestring, $countries, $tax) as $r) {
$this->rules[$method->virtuemart_shipmentmethod_id][] = $r;
}
}
public function parseRuleSyntax($rulestring, $countries, $tax) {
$result = array();
$rules1 = preg_split("/(\r\n|\n|\r)/", $rulestring);
foreach ($rules1 as $r) {
// Ignore empty lines
if (empty($r)) continue;
$result[] = $this->createMethodRule ($r, $countries, $tax);
}
return $result;
}
protected function parseMethodRules (&$method) {
if (!isset($this->rules[$method->virtuemart_shipmentmethod_id]))
$this->rules[$method->virtuemart_shipmentmethod_id] = array();
$this->parseMethodRule ($method->rules1, $method->countries1, $method->tax_id1, $method);
$this->parseMethodRule ($method->rules2, $method->countries2, $method->tax_id2, $method);
$this->parseMethodRule ($method->rules3, $method->countries3, $method->tax_id3, $method);
$this->parseMethodRule ($method->rules4, $method->countries4, $method->tax_id4, $method);
$this->parseMethodRule ($method->rules5, $method->countries5, $method->tax_id5, $method);
$this->parseMethodRule ($method->rules6, $method->countries6, $method->tax_id6, $method);
$this->parseMethodRule ($method->rules7, $method->countries7, $method->tax_id7, $method);
$this->parseMethodRule ($method->rules8, $method->countries8, $method->tax_id8, $method);
}
/** Filter the given array of products and return only those that belong to the categories, manufacturers,
* vendors or products given in the $filter_conditions. The $filter_conditions is an array of the form:
* array( 'skus'=>array(....), 'categories'=>array(1,2,3,42), 'manufacturers'=>array(77,78,83), 'vendors'=>array(1,2))
* Notice that giving an empty array for any of the keys means "no restriction" and is exactly the same
* as leaving out the enty altogether
*/
public function filterProducts($products, $filter_conditions) {
return array();
}
}
class ShippingRule {
var $framework = Null;
var $rulestring = '';
var $name = '';
var $ruletype = '';
var $evaluated = False;
var $match = False;
var $value = Null;
var $shipping = 0;
var $conditions = array();
var $countries = array();
var $tax_id = 0;
var $includes_tax = 0;
function __construct ($framework, $rule, $countries, $tax_id) {
if (is_array($countries)) {
$this->countries = $countries;
} elseif (!empty($countries)) {
$this->countries[0] = $countries;
}
$this->tax_id = $tax_id;
$this->rulestring = $rule;
$this->parseRule($rule);
$this->framework=$framework;
}
protected function parseRule($rule) {
$ruleparts=explode(';', $rule);
foreach ($ruleparts as $p) {
$this->parseRulePart($p);
}
}
protected function handleAssignment ($var, $value, $rulepart) {
switch (strtolower($var)) {
case 'name': $this->name = $value; break;
case 'shipping': $this->shipping = $value; $this->includes_tax = False; $this->ruletype='shipping'; break;
case 'shippingwithtax': $this->shipping = $value; $this->includes_tax = True; $this->ruletype='shipping'; break;
case 'variable': // Variable=... is the same as Definition=...
case 'definition': $this->name = strtolower($value); $this->ruletype = 'definition'; break;
case 'value': $this->shipping = $value; $this->ruletype = 'definition'; break; // definition values are also stored in the shipping member!
case 'extrashippingcharge': $this->shipping = $value; $this->ruletype = 'modifiers_add'; break; // modifiers are also stored in the shipping member!
case 'extrashippingmultiplier': $this->shipping = $value; $this->ruletype = 'modifiers_multiply'; break; // modifiers are also stored in the shipping member!
case 'comment': break; // Completely ignore all comments!
case 'condition': $this->conditions[] = $value; break;
default: JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_UNKNOWN_VARIABLE', $var, $rulepart), 'error');
}
}
protected function tokenize_expression ($expression) {
// First, extract all strings, delimited by quotes, then all text operators
// (OR, AND, in; but make sure we don't capture parts of words, so we need to
// use lookbehind/lookahead patterns to exclude OR following another letter
// or followed by another letter) and then all arithmetic operators
$re = '/\s*("[^"]*"|\'[^\']*\'|<=|=>|>=|=<|<>|!=|==|<|=|>)\s*/i';
$atoms = preg_split($re, $expression, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
// JFactory::getApplication()->enqueueMessage("TOKENIZING '$expression' returns: <pre>".print_r($atoms,1)."</pre>", 'error');
return $atoms;
}
protected function parseRulePart($rulepart) {
/* In the basic version, we only split at the comparison operators and assume each term on the LHS and RHS is one variable or constant */
/* In the advanced version, all conditions and costs can be given as a full mathematical expression */
/* Both versions create an expression tree, which can be easily evaluated in evaluateTerm */
$rulepart = trim($rulepart);
if (empty($rulepart)) return;
// Special-case the name assignment, where we don't want to interpret the value as an arithmetic expression!
if (preg_match('/^\s*(name|variable|definition)\s*=\s*(["\']?)(.*)\2\s*$/i', $rulepart, $matches)) {
$this->handleAssignment ($matches[1], $matches[3], $rulepart);
return;
}
// Split at all operators:
$atoms = $this->tokenize_expression ($rulepart);
/* Starting from here, the advanced plugin is different! */
$operators = array('<', '<=', '=', '>', '>=', '=>', '=<', '<>', '!=', '==');
if (count($atoms)==1) {
$this->shipping = $this->parseShippingTerm($atoms[0]);
$this->ruletype = 'shipping';
} elseif ($atoms[1]=='=') {
$this->handleAssignment ($atoms[0], $atoms[2], $rulepart);
} else {
// Conditions, need at least three atoms!
while (count($atoms)>1) {
if (in_array ($atoms[1], $operators)) {
$this->conditions[] = array($atoms[1], $this->parseShippingTerm($atoms[0]), $this->parseShippingTerm($atoms[2]));
array_shift($atoms);
array_shift($atoms);
} else {
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_UNKNOWN_OPERATOR', $atoms[1], $rulepart), 'error');
$atoms = array();
}
}
}
}
protected function parseShippingTerm($expr) {
/* In the advanced version, shipping cost can be given as a full mathematical expression */
// If the shipping term starts with a double quote, it is a string, so don't turn it into lowercase.
// All other expressions need to be turned into lowercase, because variable names are case-insensitive!
if (substr($expr, 0, 1) === '"') {
return $expr;
} else {
return strtolower($expr);
}
}
protected function evaluateComparison ($terms, $vals) {
while (count($terms)>2) {
$res = false;
switch ($terms[1]) {
case '<': $res = ($terms[0] < $terms[2]); break;
case '<=':
case '=<': $res = ($terms[0] <= $terms[2]); break;
case '==': $res = is_equal($terms[0], $terms[2]); break;
case '!=':
case '<>': $res = ($terms[0] != $terms[2]); break;
case '>=':
case '=>': $res = ($terms[0] >= $terms[2]); break;
case '>': $res = ($terms[0] > $terms[2]); break;
case '~':
$l=min(strlen($terms[0]), strlen($terms[2]));
$res = (strncmp ($terms[0], $terms[2], $l) == 0);
break;
default:
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_UNKNOWN_OPERATOR', $terms[1], $this->rulestring), 'error');
$res = false;
}
if ($res==false) return false;
// Remove the first operand and the operator from the comparison:
array_shift($terms);
array_shift($terms);
}
if (count($terms)>1) {
// We do not have the correct number of terms for chained comparisons, i.e. two terms leftover instead of one!
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_ERROR', $this->rulestring), 'error');
return false;
}
// All conditions were fulfilled, so we can return true
return true;
}
protected function evaluateListFunction ($function, $args) {
# First make sure that all arguments are actually lists:
$allarrays = True;
foreach ($args as $a) {
$allarrays = $allarrays && is_array($a);
}
if (!$allarrays) {
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_LISTFUNCTION_ARGS', $function, $this->rulestring), 'error');
return false;
}
switch ($function) {
case "length": return count($args[0]); break;
case "union":
case "join": return call_user_func_array( "array_merge" , $args); break;
case "complement": return call_user_func_array( "array_diff" , $args); break;
case "intersection": return call_user_func_array( "array_intersect" , $args); break;
case "issubset": # Remove all of superset's elements to see if anything else is left:
return !array_diff($args[0], $args[1]); break;
case "contains": # Remove all of superset's elements to see if anything else is left:
# Notice the different argument order compared to issubset!
return !array_diff($args[1], $args[0]); break;
case "list_equal": return array_unique($args[0])==array_unique($args[1]); break;
default:
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_LISTFUNCTION_UNKNOWN', $function, $this->rulestring), 'error');
return false;
}
}
protected function evaluateListContainmentFunction ($function, $args) {
# First make sure that the first argument is a list:
if (!is_array($args[0])) {
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_LISTFUNCTION_CONTAIN_ARGS', $function, $this->rulestring), 'error');
return false;
}
// Extract the array from the args, the $args varialbe will now only contain the elements to be checked:
$array = array_shift($args);
switch ($function) {
case "contains_any": // return true if one of the $args is in the $array
foreach ($args as $a) {
if (in_array($a, $array))
return true;
}
return false;
case "contains_all": // return false if one of the $args is NOT in the $array
foreach ($args as $a) {
if (!in_array($a, $array))
return false;
}
return true;
case "contains_only": // return false if one of the $array elements is NOT in $args
foreach ($array as $a) {
if (!in_array($a, $args))
return false;
}
return true;
case "contains_none": // return false if one of the $args IS in the $array
foreach ($args as $a) {
if (in_array($a, $array))
return false;
}
return true;
default:
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_LISTFUNCTION_UNKNOWN', $function, $this->rulestring), 'error');
return false;
}
}
/** Evaluate the given expression $expr only for the products that match the filter given by the scoping
* function and the corresponding conditions */
protected function evaluateScoping($expr, $scoping, $conditionvals, $vals, $products, $cartvals_callback) {
if (count($conditionvals)<1)
return $this->evaluateTerm($expr, $vals, $products, $cartvals_callback);
$filterkeys = array(
"evaluate_for_categories" => 'categories',
"evaluate_for_products" => 'products',
"evaluate_for_vendors" => 'vendors',
"evaluate_for_manufacturers" => 'manufacturers'
);
$conditions = array();
if (isset($filterkeys[$scoping]))
$conditions[$filterkeys[$scoping]] = $conditionvals;
// Pass the conditions to the parent plugin class to filter the current list of products:
$filteredproducts = $this->framework->filterProducts($products, $conditions);
// We have been handed a callback function to calculate the cartvals for the filtered list of products, so use it:
$filteredvals = $cartvals_callback($filteredproducts);
return $this->evaluateTerm ($expr, $filteredvals, $filteredproducts, $cartvals_callback);
}
protected function evaluateFunction ($function, $args) {
$func = strtolower($function);
// Check if we have a custom function definition and use that if so.
// This is done first to allow plugins to override even built-in functions!
if (isset($this->plugin->custom_functions[$func])) {
vmDebug("Evaluating custom function $function, defined by a plugin");
return call_user_func($this->plugin->custom_functions[$func], $args, $this);
}
// Functions with no argument:
if (count($args) == 0) {
$dt = getdate();
switch ($func) {
case "second": return $dt['seconds']; break;
case "minute": return $dt['minutes']; break;
case "hour": return $dt['hours']; break;
case "day": return $dt['mday']; break;
case "weekday":return $dt['wday']; break;
case "month": return $dt['mon']; break;
case "year": return $dt['year']; break;
case "yearday":return $dt['yday']; break;
}
}
// Functions with exactly one argument:
if (count($args) == 1) {
switch ($func) {
case "round": return round($args[0]); break;
case "ceil": return ceil ($args[0]); break;
case "floor": return floor($args[0]); break;
case "abs": return abs($args[0]); break;
case "not": return !$args[0]; break;
case "print_r": return print_r($args[0],1); break;
}
}
if (count($args) == 2) {
switch ($func) {
case "digit": return substr($args[0], $args[1]-1, 1); break;
case "round": return round($args[0]/$args[1])*$args[1]; break;
case "ceil": return ceil($args[0]/$args[1])*$args[1]; break;
case "floor": return floor($args[0]/$args[1])*$args[1]; break;
}
}
if (count($args) == 3) {
switch ($func) {
case "substring": return substr($args[0], $args[1]-1, $args[2]); break;
}
}
// Functions with variable number of args
switch ($func) {
case "max":
return max($args);
case "min":
return min($args);
case "list":
case "array":
return $args;
// List functions:
case "length":
case "complement":
case "issubset":
case "contains":
case "union":
case "join":
case "intersection":
case "list_equal":
return $this->evaluateListFunction ($func, $args);
case "contains_any":
case "contains_all":
case "contains_only":
case "contains_none":
return $this->evaluateListContainmentFunction($func, $args);
}
// None of the built-in function
// No known function matches => print an error, return 0
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_FUNCTION', $function, $this->rulestring), 'error');
return 0;
}
protected function evaluateVariable ($expr, $vals) {
$varname = strtolower($expr);
if (array_key_exists(strtolower($expr), $vals)) {
return $vals[strtolower($expr)];
} elseif ($varname=='noshipping') {
return $varname;
} elseif ($varname=='values') {
return $vals;
} elseif ($varname=='values_debug') {
return print_r($vals,1);
} else {
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_VALUE', $expr, $this->rulestring), 'error');
return null;
}
}
protected function evaluateTerm ($expr, $vals, $products, $cartvals_callback) {
// The scoping functions need to be handled differently, because they first need to adjust the cart variables to the filtered product list
// before evaluating its first argument. So even though parsing the rules handles scoping functions like any other function, their
// evaluation is fundamentally different and is special-cased here:
$scoping_functions = array("evaluate_for_categories", "evaluate_for_products", "evaluate_for_vendors", "evaluate_for_manufacturers");
$is_scoping = is_array($expr) && ($expr[0]=="FUNCTION") && (count($expr)>1) && in_array($expr[1], $scoping_functions);
if (is_null($expr)) {
return $expr;
} elseif (is_numeric ($expr)) {
return $expr;
} elseif (is_string ($expr)) {
// Explicit strings are delimited by '...' or "..."
if (($expr[0]=='\'' || $expr[0]=='"') && ($expr[0]==substr($expr,-1)) ) {
return substr($expr,1,-1);
} else {
return $this->evaluateVariable($expr, $vals);
}
} elseif ($is_scoping) {
$op = array_shift($expr); // ignore the "FUNCTION"
$func = array_shift($expr); // The scoping function name
$expression = array_shift($expr); // The expression to be evaluated
$conditions = $expr; // the remaining $expr list now contains the conditions
return $this->evaluateScoping ($expression, $func, $conditions, $vals, $products, $cartvals_callback);
} elseif (is_array($expr)) {
// Operator
$op = array_shift($expr);
$args = array();
// First evaluate all operands and only after that apply the function / operator to the already evaluated arguments
$evaluate = true;
if ($op == "FUNCTION") {
$evaluate = false;
}
foreach ($expr as $e) {
$term = $evaluate ? ($this->evaluateTerm($e, $vals, $products, $cartvals_callback)) : $e;
if ($op == 'COMPARISON') {
// For comparisons, we only evaluate every other term (the operators are NOT evaluated!)
// The data format for comparisons is: array('COMPARISON', $operand1, '<', $operand2, '<=', ....)
$evaluate = !$evaluate;
}
if ($op == "FUNCTION") {
$evaluate = true;
}
if (is_null($term)) return null;
$args[] = $term;
}
$res = false;
// Finally apply the operaton to the evaluated argument values:
switch ($op) {
// Logical operators:
case 'OR': foreach ($args as $a) { $res = ($res || $a); }; break;
case '&&':
case 'AND': $res = true; foreach ($args as $a) { $res = ($res && $a); }; break;
case 'IN': $res = in_array($args[0], $args[1]); break;
// Comparisons:
case '<':
case '<=':
case '=<':
case '==':
case '!=':
case '<>':
case '>=':
case '=>':
case '>':
case '~':
$res = $this->evaluateComparison(array($args[0], $op, $args[1]), $vals); break;
case 'COMPARISON':
$res = $this->evaluateComparison($args, $vals); break;
// Unary operators:
case '.-': $res = -$args[0]; break;
case '.+': $res = $args[0]; break;
// Binary operators
case "+": $res = ($args[0] + $args[1]); break;
case "-": $res = ($args[0] - $args[1]); break;
case "*": $res = ($args[0] * $args[1]); break;
case "/": $res = ($args[0] / $args[1]); break;
case "%": $res = (fmod($args[0], $args[1])); break;
case "^": $res = ($args[0] ^ $args[1]); break;
// Functions:
case "FUNCTION": $func = array_shift($args); $res = $this->evaluateFunction($func, $args); break;
default: $res = false;
}
// JFactory::getApplication()->enqueueMessage("<pre>Result of ".print_r($expr,1)." is $res.</pre>", 'error');
return $res;
} else {
// Neither string nor numeric, nor operator...
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_VALUE', $expr, $this->rulestring), 'error');
return null;
}
}
protected function calculateShipping ($vals, $products, $cartvals_callback) {
return $this->evaluateTerm($this->shipping, $vals, $products, $cartvals_callback);
}
protected function evaluateRule (&$vals, $products, $cartvals_callback) {
if ($this->evaluated)
return; // Already evaluated
$this->evaluated = True;
$this->match = False; // Default, set it to True below if all conditions match...
// First, check the country, if any conditions are given:
if (count ($this->countries) > 0 && !in_array ($vals['countryid'], $this->countries)) {
// vmdebug('Rule::matches: Country check failed: countryid='.print_r($vals['countryid'],1).', countries are: '.print_r($this->countries,1).'...');
return;
}
foreach ($this->conditions as $c) {
// All conditions have to match!
$ret = $this->evaluateTerm($c, $vals, $products, $cartvals_callback);
if (is_null($ret) || (!$ret)) {
return;
}
}
// All conditions match
$this->match = True;
// Calculate the value (i.e. shipping cost or modifier)
$this->value = $this->calculateShipping($vals, $products, $cartvals_callback);
// Evaluate the rule name as a translatable string with variables inserted:
// Replace all {variable} tags in the name by the variables from $vals
$matches=array();
$name=JText::_($this->name);
preg_match_all('/{([A-Za-z0-9_]+)}/', $name, $matches);
foreach ($matches[1] as $m) {
$val = $this->evaluateVariable($m, $vals);
if ($val !== null) {
$name = str_replace("{".$m."}", $val, $name);
}
}
$this->rulename = $name;
}
function matches(&$vals, $products, $cartvals_callback) {
$this->evaluateRule($vals, $products, $cartvals_callback);
return $this->match;
}
function getType() {
return $this->ruletype;
}
function getRuleName() {
if (!$this->evaluated)
vmDebug('WARNING: getRuleName called without prior evaluation of the rule, e.g. by calling rule->matches(...)');
return $this->rulename;
}
function getValue() {
if (!$this->evaluated)
vmDebug('WARNING: getValue called without prior evaluation of the rule, e.g. by calling rule->matches(...)');
return $this->value;
}
function getShippingCosts() {
return $this->getValue();
}
function isNoShipping() {
// NoShipping is set, so if the rule matches, this method should not offer any shipping at all
return (is_string($this->shipping) && (strtolower($this->shipping)=="noshipping"));
}
}
/** Extend the shipping rules by allowing arbitrary mathematical expressions
*/
class ShippingRule_Advanced extends ShippingRule {
function __construct ($method, $rule, $countries, $tax_id) {
parent::__construct ($method, $rule, $countries, $tax_id);
}
function tokenize_expression ($expression) {
// First, extract all strings, delimited by quotes, then all text operators
// (OR, AND, in; but make sure we don't capture parts of words, so we need to
// use lookbehind/lookahead patterns to exclude OR following another letter
// or followed by another letter) and then all arithmetic operators
$re = '/\s*("[^"]*"|\'[^\']*\'|(?<![A-Za-z0-9])(?:OR|AND|IN)(?![A-Za-z0-9])|&&|<=|=>|>=|=<|<>|!=|==|<|=|>|~|\+|-|\*|\/|%|\(|\)|\^|,)\s*/i';
$atoms = preg_split($re, $expression, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
// JFactory::getApplication()->enqueueMessage("TOKENIZING '$expression' returns: <pre>".print_r($atoms,1)."</pre>", 'error');
return $atoms;
}
/** parse the mathematical expressions using the Shunting Yard Algorithm by Dijkstra (with some extensions to allow arbitrary functions):
* First parse the string into an array of tokens (operators and operands) by a simple regexp with known operators as separators)
* TODO: Update this description to include unary operators and general function calls
* Then convert the infix notation into postfix (RPN), taking care of operator precedence
* 1) Initialize empty stack and empty result variable
* 2) Read infix expression from left to right, one atom at a time
* 3) If operand => Append to result
* 4) If operator:
* 4a) Pop operators from stack until opening parenthesis, operator of
* lower precedence or right-associative symbol of equal precedence.
* 4b) Push operator onto stack
* 5) If opening parenthesis => push onto stack
* 6) If closing parenthesis:
* 6a) Pop operators from stack until opening parenthesis is found
* 6b) push them to the result (not the opening parenthesis, of course)
* 7) At the end of the input, pop all operators from the stack and onto the result
*
* Afterwards, convert this RPN list into an expression tree to be evaluated
*
* For the full algorithm, including function parsing, see Wikipedia:
* http://en.wikipedia.org/wiki/Shunting_yard_algorithm
*
*/
function parseRulePart($rulepart) {
/* In the basic version, we only split at the comparison operators and assume each term on the LHS and RHS is one variable or constant */
/* In the advanced version, all conditions and costs can be given as a full mathematical expression */
/* Both versions create an expression tree, which can be easily evaluated in evaluateTerm */
$rulepart = trim($rulepart);
if (!isset($rulepart) || $rulepart==='') return;
// Special-case the name assignment, where we don't want to interpret the value as an arithmetic expression!
if (preg_match('/^\s*(name|variable|definition)\s*=\s*(["\']?)(.*)\2\s*$/i', $rulepart, $matches)) {
$this->handleAssignment ($matches[1], $matches[3], $rulepart);
return;
}
// Split at all operators:
$atoms = $this->tokenize_expression ($rulepart);
$operators = array(
".-" => 100, ".+" => 100,
"IN" => 80,
"^" => 70,
"*" => 60, "/" => 60, "%" => 60,
"+" => 50, "-" => 50,
"<" => 40, "<=" => 40, ">" => 40, ">=" => 40, "=>" => 40, "=<" => 40,
"==" => 40, "!=" => 40, "<>" => 40, "~" => 40,
"&&" => 21, "AND" => 21,
"OR" => 20,
"=" => 10,
"(" => 0, ")" =>0
);
$unary_ops = array("-" => ".-", "+" => ".+");
// Any of these indicate a comparison and thus a condition:
$condition_ops = array('<', '<=', '=<', '<>', '!=', '==', '>', '>=', '=>', '~', 'OR', 'AND', '&&', 'IN');
$comparison_ops = array('<', '<=', '=<', '<>', '!=', '==', '>', '>=', '=>', '~');
$is_condition = false;
$is_assignment = false;
$stack = array (); // 1)/
$prev_token_operator = true;
$function_args = array();
$out_stack = array();
foreach ($atoms as $a) { // 2)
$aupper = strtoupper($a); # All operators are converted to uppercase!
if ($a == ",") { // A function argument separator
// pop-and-apply all operators back to the left function paren
while (count($stack)>0) { // 4a)
$op = array_pop ($stack);
if ($op != "FUNCTION(") {
array_push ($out_stack, $op);
} else {
// No unary operator -> add it back to stack, exit loop
array_push ($stack, $op);
break;
}
} while (0);
$this_func = array_pop($function_args);
// Add current output stack as argument, reset temporary output stack
if (!empty($out_stack)) $this_func[] = $out_stack;
$function_args[] = $this_func;
$out_stack = array();
$prev_token_operator = true;
} elseif ($a == "(" and !$prev_token_operator) { // 5) parenthesis after operand -> FUNCTION CALL
array_push ($stack, "FUNCTION(");
// retrieve function name from RPN list (remove last entry from operand stack!)
$function = strtolower(array_pop ($out_stack));
$new_stack = array();
// Set up function call data structure on function_args stack:
$function_args[] = array(/* old operand stack: */$out_stack, $function);
// Use a the temporary operand stack until the closing paren restores the previous operand stack again
$out_stack = array();
$prev_token_operator = true;
} elseif ($a == "(" and $prev_token_operator) { // 5) real parenthesis
$stack[] = $a;
$prev_token_operator = true;
} elseif ($a == ")") { // 6) parenthesis
do {
$op=array_pop($stack); // 6a)
if ($op == "(") {
break; // We have found the opening parenthesis
} elseif ($op =="FUNCTION(") { // Function call
// Remove function info from the functions stack; Format is array(PREVIOUS_OPERAND_STACK, FUNCTION, ARGS...)
$this_func = array_pop ($function_args);
// Append last argument (if not empty)
if (!empty($out_stack)) $this_func[] = $out_stack;
// restore old output/operand stack
$out_stack = array_shift($this_func);
// Function name is the next entry
$function = array_shift($this_func);
// All other entries are function arguments, so append them to the current operand stack
foreach ($this_func as $a) {
foreach ($a as $aa) {
$out_stack[] = $aa;
}
}
$out_stack[] = array("FUNCTION", $function, count($this_func));
break; // We have found the opening parenthesis
} elseif (!is_null($op)) {
$out_stack[]=$op; // 6b) "normal" operators
} else {
// no ( and no operator, so the expression is wrong!
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_PARSE_MISSING_PAREN', $rulepart), 'error');
break;
}
} while (true);
$prev_token_operator = false;
} elseif (isset($unary_ops[$aupper]) && $prev_token_operator) { // 4) UNARY operators
// Unary and binary operators need to be handled differently:
// Unary operators must only pop other unary operators, never any binary operator
$unary_op = $unary_ops[$aupper];
// For unary operators, pop other unary operators from the stack until you reach an opening parenthesis,
// an operator of lower precedence, or a right associative symbol of equal precedence.
while (count($stack)>0) { // 4a)
$op = array_pop ($stack);
// Remove all other unary operators:
if (in_array ($op, $unary_ops)) {
array_push ($out_stack, $op);
} else {
// No unary operator -> add it back to stack, exit loop
array_push ($stack, $op);
break;
}
} while (0);
array_push ($stack, $unary_op); // 4b)
$prev_token_operator = true;
} elseif (isset($operators[$aupper])) { // 4) BINARY operators
$prec = $operators[$aupper];
$is_condition |= in_array($aupper, $condition_ops);
$is_assignment |= ($aupper == "=");
// For operators, pop operators from the stack until you reach an opening parenthesis,
// an operator of lower precedence, or a right associative symbol of equal precedence.
while (count($stack)>0) { // 4a)
$op = array_pop ($stack);
// The only right-associative operator is =, which we allow at most once!
if ($op == "(" || $op == "FUNCTION(") {
// add it back to the stack!
array_push ($stack, $op);
break;
} elseif ($operators[$op]<$prec) {
// We found an operator with lower precedence, add it back to the stack!
array_push ($stack, $op); // 4b)
break;
} else {
array_push ($out_stack, $op);
}
} while (0);
array_push ($stack, $aupper); // 4b)
$prev_token_operator = true;
} else { // 3) Everything else is an Operand
$out_stack[] = $a;
$prev_token_operator = false;
}
}
// Finally, pop all operators from the stack and append them to the result
while ($op=array_pop($stack)) {
// Opening parentheses should not be found on the stack any more. That would mean a closing paren is missing!
if ($op == "(") {
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_PARSE_PAREN_NOT_CLOSED', $rulepart), 'error');
} else {
array_push ($out_stack, $op);
}
}
if (!empty($function_args)) {
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_PARSE_FUNCTION_NOT_CLOSED', $rulepart), 'error');
}
/** Now, turn the RPN into an expression tree (i.e. "evaluate" it into a tree structure), according to Knuth:
* 1) Initialize an empty stack
* 2) Read the RPN from left to right
* 3) If operand, push it onto the stack
* 4) If operator:
* 4a) pop two operands
* 4b) perform operation
* 4c) push result onto stack
* 4d) (If less than two operands => ERROR, invalid syntax)
* 5) At the end of the RPN, pop the result from the stack.
* 5a) The stack should now be empty (otherwise, ERROR, invalid syntax)
*/
$stack=array(); // 1)
foreach ($out_stack as $e) { // 2)
if (is_array($e) && $e[0]=="FUNCTION") { // A function call (#args is saved as $e[2], so remove that number of operands from the stack)
$function = $e[1];
$argc = $e[2];
$args = array();
for ($i = $argc; $i > 0; $i--) {
$a = array_pop($stack);
array_unshift($args, $a);
}
array_unshift($args, $function);
array_unshift($args, "FUNCTION");
$stack[] = $args;
} elseif (in_array($e, $unary_ops)) { // 4) unary operators
// Operator => apply to the last value on the stack
if (count($stack)<1) { // 4d)
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_SYNTAXERROR', $rulepart), 'error');
array_push($stack, 0);
continue;
}
$o1 = array_pop($stack);
// Special-case chained comparisons: if e is a comparison, and operator(o1) is also a comparison,
// insert the arguments to the existing comparison instead of creating a new one
$op = array ($e, $o1); // 4b)
array_push ($stack, $op); // 4c)
} elseif (isset($operators[$e])) { // 4) binary operators
// Operator => apply to the last two values on the stack
if (count($stack)<2) { // 4d)
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_SYNTAXERROR', $rulepart), 'error');
array_push($stack, 0);
continue;
}
$o2 = array_pop($stack); // 4a)
$o1 = array_pop($stack);
// Special-case chained comparisons, e.g. 1<=Amount<100:
// if e is a comparison, and operator(o1) is also a comparison,
// insert the arguments to the existing comparison instead of creating a new one
if (in_array ($e, $comparison_ops)) {
if ($o1[0]=='COMPARISON') {
$op = $o1;
// Append the new comparison to the existing one
array_push($op, $e, $o2);
} else {
$op = array ('COMPARISON', $o1, $e, $o2);
}
} else {
$op = array ($e, $o1, $o2); // 4b)
}
array_push ($stack, $op); // 4c)
} else { // 3)
// Operand => push onto stack
array_push ($stack, $e);
}
}
// 5a)
if (count($stack) != 1) {
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_ERROR', $rulepart), 'error');
JFactory::getApplication()->enqueueMessage(JText::sprintf('Outstack: <pre>%s</pre>', print_r($out_stack,1)), 'error');
$stack = array (0);
}
$res = array_pop($stack); // 5)
if ($is_assignment) { // Assignments are handled first, so conditions can be assigned to variables
if ($res[0]=='=') {
$this->handleAssignment ($res[1], $res[2], $rulepart);
} else {
// Assignment has to be top-level!
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_ASSIGNMENT_TOPLEVEL', $rulepart), 'error');
}
} elseif ($is_condition) { // Comparisons are conditions
$this->conditions[] = $res;
} else {
// Terms without comparisons or assignments are shipping cost expressions
$this->shipping = $res;
$this->ruletype = 'shipping';
$this->includes_tax = False;
}
// JFactory::getApplication()->enqueueMessage("<pre>Rule part '$rulepart' (type $this->ruletype) parsed into (condition=".print_r($is_condition,1).", assignment=".print_r($is_assignment,1)."): ".print_r($res,1)."</pre>", 'error');
}
}
// No closing tag
......@@ -27,23 +27,6 @@ if (!class_exists ('plgVmShipmentRules_Shipping_Base')) {
}
function print_array($obj) {
$res = "";
if (is_array($obj)) {
$res .= "array(";
$sep = "";
foreach ($obj as $e) {
$res .= $sep . print_array($e);
$sep = ", ";
}
$res .= ")";
} elseif (is_string($obj)) {
$res .= "\"$obj\"";
} else {
$res .= (string)$obj;
}
return $res;
}
/** Shipping costs according to general rules.
......@@ -52,8 +35,10 @@ function print_array($obj) {
class plgVmShipmentRules_Shipping_Advanced extends plgVmShipmentRules_Shipping_Base {
function __construct (& $subject, $config) {
parent::__construct ($subject, $config);
$this->helper->registerCallback('initRule', array($this, 'initRule'));
$this->helper->registerCallback('addCustomCartValues', array($this, 'addCustomCartValues'));
}
protected function createMethodRule ($r, $countries, $tax) {
protected function initRule ($r, $countries, $tax) {
return new ShippingRule_Advanced ($this, $r, $countries, $tax);
}
/** Allow child classes to add additional variables for the rules
......@@ -97,338 +82,3 @@ class plgVmShipmentRules_Shipping_Advanced extends plgVmShipmentRules_Shipping_B
}
}
/** Extend the shipping rules by allowing arbitrary mathematical expressions
*/
class ShippingRule_Advanced extends ShippingRule {
function __construct ($method, $rule, $countries, $tax_id) {
parent::__construct ($method, $rule, $countries, $tax_id);
}
function tokenize_expression ($expression) {
// First, extract all strings, delimited by quotes, then all text operators
// (OR, AND, in; but make sure we don't capture parts of words, so we need to
// use lookbehind/lookahead patterns to exclude OR following another letter
// or followed by another letter) and then all arithmetic operators
$re = '/\s*("[^"]*"|\'[^\']*\'|(?<![A-Za-z0-9])(?:OR|AND|IN)(?![A-Za-z0-9])|&&|<=|=>|>=|=<|<>|!=|==|<|=|>|~|\+|-|\*|\/|%|\(|\)|\^|,)\s*/i';
$atoms = preg_split($re, $expression, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
// JFactory::getApplication()->enqueueMessage("TOKENIZING '$expression' returns: <pre>".print_r($atoms,1)."</pre>", 'error');
return $atoms;
}
/** parse the mathematical expressions using the Shunting Yard Algorithm by Dijkstra (with some extensions to allow arbitrary functions):
* First parse the string into an array of tokens (operators and operands) by a simple regexp with known operators as separators)
* TODO: Update this description to include unary operators and general function calls
* Then convert the infix notation into postfix (RPN), taking care of operator precedence
* 1) Initialize empty stack and empty result variable
* 2) Read infix expression from left to right, one atom at a time
* 3) If operand => Append to result
* 4) If operator:
* 4a) Pop operators from stack until opening parenthesis, operator of
* lower precedence or right-associative symbol of equal precedence.
* 4b) Push operator onto stack
* 5) If opening parenthesis => push onto stack
* 6) If closing parenthesis:
* 6a) Pop operators from stack until opening parenthesis is found
* 6b) push them to the result (not the opening parenthesis, of course)
* 7) At the end of the input, pop all operators from the stack and onto the result
*
* Afterwards, convert this RPN list into an expression tree to be evaluated
*
* For the full algorithm, including function parsing, see Wikipedia:
* http://en.wikipedia.org/wiki/Shunting_yard_algorithm
*
*/
function parseRulePart($rulepart) {
/* In the basic version, we only split at the comparison operators and assume each term on the LHS and RHS is one variable or constant */
/* In the advanced version, all conditions and costs can be given as a full mathematical expression */
/* Both versions create an expression tree, which can be easily evaluated in evaluateTerm */
$rulepart = trim($rulepart);
if (!isset($rulepart) || $rulepart==='') return;
// Special-case the name assignment, where we don't want to interpret the value as an arithmetic expression!
if (preg_match('/^\s*(name|variable|definition)\s*=\s*(["\']?)(.*)\2\s*$/i', $rulepart, $matches)) {
$this->handleAssignment ($matches[1], $matches[3], $rulepart);
return;
}
// Split at all operators:
$atoms = $this->tokenize_expression ($rulepart);
$operators = array(
".-" => 100, ".+" => 100,
"IN" => 80,
"^" => 70,
"*" => 60, "/" => 60, "%" => 60,
"+" => 50, "-" => 50,
"<" => 40, "<=" => 40, ">" => 40, ">=" => 40, "=>" => 40, "=<" => 40,
"==" => 40, "!=" => 40, "<>" => 40, "~" => 40,
"&&" => 21, "AND" => 21,
"OR" => 20,
"=" => 10,
"(" => 0, ")" =>0
);
$unary_ops = array("-" => ".-", "+" => ".+");
// Any of these indicate a comparison and thus a condition:
$condition_ops = array('<', '<=', '=<', '<>', '!=', '==', '>', '>=', '=>', '~', 'OR', 'AND', '&&', 'IN');
$comparison_ops = array('<', '<=', '=<', '<>', '!=', '==', '>', '>=', '=>', '~');
$is_condition = false;
$is_assignment = false;
$stack = array (); // 1)/
$prev_token_operator = true;
$function_args = array();
$out_stack = array();
foreach ($atoms as $a) { // 2)
$aupper = strtoupper($a); # All operators are converted to uppercase!
if ($a == ",") { // A function argument separator
// pop-and-apply all operators back to the left function paren
while (count($stack)>0) { // 4a)
$op = array_pop ($stack);
if ($op != "FUNCTION(") {
array_push ($out_stack, $op);
} else {
// No unary operator -> add it back to stack, exit loop
array_push ($stack, $op);
break;
}
} while (0);
$this_func = array_pop($function_args);
// Add current output stack as argument, reset temporary output stack
if (!empty($out_stack)) $this_func[] = $out_stack;
$function_args[] = $this_func;
$out_stack = array();
$prev_token_operator = true;
} elseif ($a == "(" and !$prev_token_operator) { // 5) parenthesis after operand -> FUNCTION CALL
array_push ($stack, "FUNCTION(");
// retrieve function name from RPN list (remove last entry from operand stack!)
$function = strtolower(array_pop ($out_stack));
$new_stack = array();
// Set up function call data structure on function_args stack:
$function_args[] = array(/* old operand stack: */$out_stack, $function);
// Use a the temporary operand stack until the closing paren restores the previous operand stack again
$out_stack = array();
$prev_token_operator = true;
} elseif ($a == "(" and $prev_token_operator) { // 5) real parenthesis
$stack[] = $a;
$prev_token_operator = true;
} elseif ($a == ")") { // 6) parenthesis
do {
$op=array_pop($stack); // 6a)
if ($op == "(") {
break; // We have found the opening parenthesis
} elseif ($op =="FUNCTION(") { // Function call
// Remove function info from the functions stack; Format is array(PREVIOUS_OPERAND_STACK, FUNCTION, ARGS...)
$this_func = array_pop ($function_args);
// Append last argument (if not empty)
if (!empty($out_stack)) $this_func[] = $out_stack;
// restore old output/operand stack
$out_stack = array_shift($this_func);
// Function name is the next entry
$function = array_shift($this_func);
// All other entries are function arguments, so append them to the current operand stack
foreach ($this_func as $a) {
foreach ($a as $aa) {
$out_stack[] = $aa;
}
}
$out_stack[] = array("FUNCTION", $function, count($this_func));
break; // We have found the opening parenthesis
} elseif (!is_null($op)) {
$out_stack[]=$op; // 6b) "normal" operators
} else {
// no ( and no operator, so the expression is wrong!
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_PARSE_MISSING_PAREN', $rulepart), 'error');
break;
}
} while (true);
$prev_token_operator = false;
} elseif (isset($unary_ops[$aupper]) && $prev_token_operator) { // 4) UNARY operators
// Unary and binary operators need to be handled differently:
// Unary operators must only pop other unary operators, never any binary operator
$unary_op = $unary_ops[$aupper];
// For unary operators, pop other unary operators from the stack until you reach an opening parenthesis,
// an operator of lower precedence, or a right associative symbol of equal precedence.
while (count($stack)>0) { // 4a)
$op = array_pop ($stack);
// Remove all other unary operators:
if (in_array ($op, $unary_ops)) {
array_push ($out_stack, $op);
} else {
// No unary operator -> add it back to stack, exit loop
array_push ($stack, $op);
break;
}
} while (0);
array_push ($stack, $unary_op); // 4b)
$prev_token_operator = true;
} elseif (isset($operators[$aupper])) { // 4) BINARY operators
$prec = $operators[$aupper];
$is_condition |= in_array($aupper, $condition_ops);
$is_assignment |= ($aupper == "=");
// For operators, pop operators from the stack until you reach an opening parenthesis,
// an operator of lower precedence, or a right associative symbol of equal precedence.
while (count($stack)>0) { // 4a)
$op = array_pop ($stack);
// The only right-associative operator is =, which we allow at most once!
if ($op == "(" || $op == "FUNCTION(") {
// add it back to the stack!
array_push ($stack, $op);
break;
} elseif ($operators[$op]<$prec) {
// We found an operator with lower precedence, add it back to the stack!
array_push ($stack, $op); // 4b)
break;
} else {
array_push ($out_stack, $op);
}
} while (0);
array_push ($stack, $aupper); // 4b)
$prev_token_operator = true;
} else { // 3) Everything else is an Operand
$out_stack[] = $a;
$prev_token_operator = false;
}
}
// Finally, pop all operators from the stack and append them to the result
while ($op=array_pop($stack)) {
// Opening parentheses should not be found on the stack any more. That would mean a closing paren is missing!
if ($op == "(") {
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_PARSE_PAREN_NOT_CLOSED', $rulepart), 'error');
} else {
array_push ($out_stack, $op);
}
}
if (!empty($function_args)) {
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_PARSE_FUNCTION_NOT_CLOSED', $rulepart), 'error');
}
/** Now, turn the RPN into an expression tree (i.e. "evaluate" it into a tree structure), according to Knuth:
* 1) Initialize an empty stack
* 2) Read the RPN from left to right
* 3) If operand, push it onto the stack
* 4) If operator:
* 4a) pop two operands
* 4b) perform operation
* 4c) push result onto stack
* 4d) (If less than two operands => ERROR, invalid syntax)
* 5) At the end of the RPN, pop the result from the stack.
* 5a) The stack should now be empty (otherwise, ERROR, invalid syntax)
*/
$stack=array(); // 1)
foreach ($out_stack as $e) { // 2)
if (is_array($e) && $e[0]=="FUNCTION") { // A function call (#args is saved as $e[2], so remove that number of operands from the stack)
$function = $e[1];
$argc = $e[2];
$args = array();
for ($i = $argc; $i > 0; $i--) {
$a = array_pop($stack);
array_unshift($args, $a);
}
array_unshift($args, $function);
array_unshift($args, "FUNCTION");
$stack[] = $args;
} elseif (in_array($e, $unary_ops)) { // 4) unary operators
// Operator => apply to the last value on the stack
if (count($stack)<1) { // 4d)
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_SYNTAXERROR', $rulepart), 'error');
array_push($stack, 0);
continue;
}
$o1 = array_pop($stack);
// Special-case chained comparisons: if e is a comparison, and operator(o1) is also a comparison,
// insert the arguments to the existing comparison instead of creating a new one
$op = array ($e, $o1); // 4b)
array_push ($stack, $op); // 4c)
} elseif (isset($operators[$e])) { // 4) binary operators
// Operator => apply to the last two values on the stack
if (count($stack)<2) { // 4d)
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_SYNTAXERROR', $rulepart), 'error');
array_push($stack, 0);
continue;
}
$o2 = array_pop($stack); // 4a)
$o1 = array_pop($stack);
// Special-case chained comparisons, e.g. 1<=Amount<100:
// if e is a comparison, and operator(o1) is also a comparison,
// insert the arguments to the existing comparison instead of creating a new one
if (in_array ($e, $comparison_ops)) {
if ($o1[0]=='COMPARISON') {
$op = $o1;
// Append the new comparison to the existing one
array_push($op, $e, $o2);
} else {
$op = array ('COMPARISON', $o1, $e, $o2);
}
} else {
$op = array ($e, $o1, $o2); // 4b)
}
array_push ($stack, $op); // 4c)
} else { // 3)
// Operand => push onto stack
array_push ($stack, $e);
}
}
// 5a)
if (count($stack) != 1) {
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_ERROR', $rulepart), 'error');
JFactory::getApplication()->enqueueMessage(JText::sprintf('Outstack: <pre>%s</pre>', print_r($out_stack,1)), 'error');
$stack = array (0);
}
$res = array_pop($stack); // 5)
if ($is_assignment) { // Assignments are handled first, so conditions can be assigned to variables
if ($res[0]=='=') {
$this->handleAssignment ($res[1], $res[2], $rulepart);
} else {
// Assignment has to be top-level!
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_ASSIGNMENT_TOPLEVEL', $rulepart), 'error');
}
} elseif ($is_condition) { // Comparisons are conditions
$this->conditions[] = $res;
} else {
// Terms without comparisons or assignments are shipping cost expressions
$this->shipping = $res;
$this->ruletype = 'shipping';
$this->includes_tax = False;
}
// JFactory::getApplication()->enqueueMessage("<pre>Rule part '$rulepart' (type $this->ruletype) parsed into (condition=".print_r($is_condition,1).", assignment=".print_r($is_assignment,1)."): ".print_r($res,1)."</pre>", 'error');
}
}
// No closing tag
/*$rule = new ShippingRule_Advanced("", array(), 0);
$rp="a+b+(-1+d)";
$rp="1+maxx(a,b)";
$rp="f(1,2,+)";
$rp="1+year()";
$rp = "1+max( 1,2,3,4,5) + min(9,10,101)";
$rp = "abs(0-1.9999)";
$rp="max(1,min(0,5,7), 9)";
// $rp="f()";
print_r($rule->tokenize_expression($rp));
$rule->parseRulePart($rp);
print_r($rule->conditions);
print_r($rule->shipping);
print_r($rule->evaluateTerm($rule->shipping, array()));
*/
\ No newline at end of file
......@@ -22,20 +22,11 @@ if (!class_exists ('vmPSPlugin')) {
require(JPATH_VM_PLUGINS . DS . 'vmpsplugin.php');
}
// Only declare the class once...
if (class_exists ('plgVmShipmentRules_Shipping_Base')) {
return;
}
function is_equal($a, $b) {
if (is_array($a) && is_array($b)) {
return !array_diff($a, $b) && !array_diff($b, $a);
} elseif (is_string($a) && is_string($b)) {
return strcmp($a,$b) == 0;
} else {
return $a == $b;
}
}
// if (class_exists ('plgVmShipmentRules_Shipping_Base')) {
// return;
// }
if (!class_exists('RulesShippingFrameworkJoomla'))
require_once (dirname(__FILE__) . DS . 'rules_shipping_framework_joomla.php');
/** Shipping costs according to general rules.
......@@ -43,10 +34,7 @@ function is_equal($a, $b) {
* Assignable variables: Shipping, Name
*/
class plgVmShipmentRules_Shipping_Base extends vmPSPlugin {
// Store the parsed and possibly evaluated rules for each method (method ID is used as key)
protected $rules = array();
protected $match = array();
var $custom_functions = array ();
protected $helper = null;
/**
* @param object $subject
......@@ -61,34 +49,9 @@ class plgVmShipmentRules_Shipping_Base extends vmPSPlugin {
$this->tableFields = array_keys ($this->getTableSQLFields ());
$varsToPush = $this->getVarsToPush ();
$this->setConfigParameterable ($this->_configTableFieldName, $varsToPush);
// PLUGIN FUNCTIONALITY:
// Let other plugins add custom functions!
// The onVmShippingRulesRegisterCustomFunctions() trigger is expected to return an array of the form:
// array ('functionname1' => 'function-to-be-called',
// 'functionname2' => array($classobject, 'memberfunc')),
// ...);
JPluginHelper::importPlugin('vmshipmentrules');
$dispatcher = JDispatcher::getInstance();
$custfuncdefs = $dispatcher->trigger('onVmShippingRulesRegisterCustomFunctions',array());
// Loop through the return values of all plugins:
foreach ($custfuncdefs as $custfuncs) {
if (empty($custfuncs))
continue;
if (!is_array($custfuncs)) {
$this->printWarning(JText::sprintf('VMSHIPMENT_RULES_CUSTOMFUNCTIONS_NOARRAY', $method->rule_name));
}
// Now loop through all custom function definitions of this plugin
// If a function was registered before, print a warning and use the first definition
foreach ($custfuncs as $fname => $func) {
if (isset($this->custom_functions[$fname])) {
$this->printWarning(JText::sprintf('VMSHIPMENT_RULES_CUSTOMFUNCTIONS_ALREADY_DEFINED', $fname));
} else {
vmDebug("Defining custom function $fname");
$this->custom_functions[strtolower($fname)] = $func;
}
}
}
$this->helper = new RulesShippingFrameworkJoomla();
$this->helper->setup();
}
/**
......@@ -100,17 +63,6 @@ class plgVmShipmentRules_Shipping_Base extends vmPSPlugin {
return $this->createTableSQL ('Shipment Rules Table');
}
public function printWarning($message) {
// Keep track of warning messages, so we don't print them twice:
global $printed_warnings;
if (!isset($printed_warnings))
$printed_warnings = array();
if (!in_array($message, $printed_warnings)) {
JFactory::getApplication()->enqueueMessage($message, 'error');
$printed_warnings[] = $message;
}
}
/**
* @return array
*/
......@@ -167,16 +119,17 @@ class plgVmShipmentRules_Shipping_Base extends vmPSPlugin {
}
// We need to call getCosts, because in J3 $method->rule_name and $method->cost as set in getCosts is no longer preserved.
// Instead, we simply call getCosts again, which as a side-effect sets all those members of $method.
$costs = $this->getCosts($cart,$method,$cart->cartPrices);
$costs = $this->helper->getCosts($cart,$method,$cart->cartPrices);
$rulename = $this->helper->getRuleName($method->virtuemart_shipmentmethod_id);
$variables = $this->helper->getRuleVariables($method->virtuemart_shipmentmethod_id);
$values['virtuemart_order_id'] = $order['details']['BT']->virtuemart_order_id;
$values['order_number'] = $order['details']['BT']->order_number;
$values['virtuemart_shipmentmethod_id'] = $order['details']['BT']->virtuemart_shipmentmethod_id;
$values['shipment_name'] = $this->renderPluginName ($method);
$values['rule_name'] = $method->rule_name;
$weights = $this->getOrderWeights ($cart, $cart->products, $method->weight_unit);
$values['order_weight'] = $weights['weight'];
$values['order_articles'] = $this->getOrderArticles ($cart, $cart->products);
$values['order_products'] = $this->getOrderProducts ($cart, $cart->products);
$values['rule_name'] = $rulename;
$values['order_weight'] = $variables['weight'];
$values['order_articles'] = $variables['articles'];
$values['order_products'] = $variables['products'];
$values['shipment_weight_unit'] = $method->weight_unit;
$values['shipment_cost'] = $method->cost;
$values['tax_id'] = $method->tax_id;
......@@ -184,7 +137,12 @@ class plgVmShipmentRules_Shipping_Base extends vmPSPlugin {
return TRUE;
}
function getCosts (VirtueMartCart $cart, $method, $cart_prices) {
return $this->helper->getCosts($cart, $method, $cart_prices);
}
protected function checkConditions ($cart, $method, $cart_prices) {
return $this->helper->checkConditions($cart, $method, $cart_prices);
}
/**
* This method is fired when showing the order details in the backend.
* It displays the shipment-specific data.
......@@ -265,118 +223,6 @@ class plgVmShipmentRules_Shipping_Base extends vmPSPlugin {
return $pluginName;
}
/** This function evaluates all rules, one after the other until it finds a matching rule that
* defines shipping costs (or uses NoShipping). If a modifier or definition is encountered,
* its effect is stored, but the loop continues */
protected function evaluateMethodRules ($cart, $method, $cart_prices) {
// $method->match will cache the matched rule and the modifiers
if (isset($this->match[$method->virtuemart_shipmentmethod_id])) {
return $this->match[$method->virtuemart_shipmentmethod_id];
} else {
// Evaluate all rules and find the matching ones (including modifiers and definitions!)
$result = array("rule"=>Null, "rule_name"=>"", "modifiers_add"=>array(), "modifiers_multiply"=>array());
$cartvals = $this->getCartValues ($cart, $cart->products, $method, $cart_prices);
// Pass a callback function to the rules to obtain the cartvals for a subset of the products
$this_class = $this;
$cartvals_callback = function ($products) use ($this_class, $cart, $method, $cart_prices) {
return $this_class->getCartValues ($cart, $products, $method, NULL);
};
foreach ($this->rules[$method->virtuemart_shipmentmethod_id] as $r) {
if ($r->matches($cartvals, $cart->products, $cartvals_callback)) {
$rtype = $r->getType();
switch ($rtype) {
case 'shipping':
case 'shippingwithtax':
case 'noshipping':
$result["rule"] = $r;
$result["rule_name"] = $r->getRuleName();
break;
case 'modifiers_add':
case 'modifiers_multiply':
$result[$rtype][] = $r;
break;
case 'definition': // A definition updates the $cartvals, but has no other effects
$cartvals[strtolower($r->getRuleName())] = $r->getValue();
break;
default:
$this->printWarning(JText::sprintf('VMSHIPMENT_RULES_UNKNOWN_TYPE', $r->getType(), $r->rulestring));
break;
}
}
if (!is_null($result["rule"])) {
$this->match[$method->virtuemart_shipmentmethod_id] = $result;
return $result; // <- This also breaks out of the foreach loop!
}
}
}
// None of the rules matched, so return NULL, but keep the evaluated results;
$this->match[$method->virtuemart_shipmentmethod_id] = $result;
return NULL;
}
/**
* @param \VirtueMartCart $cart
* @param int $method
* @param array $cart_prices
* @return bool
*/
protected function checkConditions ($cart, $method, $cart_prices) {
if (!isset($this->rules[$method->virtuemart_shipmentmethod_id]))
$this->parseMethodRules($method);
$match = $this->evaluateMethodRules ($cart, $method, $cart_prices);
if ($match && !is_null ($match['rule'])) {
$method->rule_name = $match["rule_name"];
// If NoShipping is set, this method should NOT offer any shipping at all, so return FALSE, otherwise TRUE
// If the rule has a name, print it as warning (otherwise don't print anything)
if ($match['rule']->isNoShipping()) {
if (!empty($method->rule_name))
$this->printWarning(JText::sprintf('VMSHIPMENT_RULES_NOSHIPPING_MESSAGE', $method->rule_name));
vmdebug('checkConditions '.$method->shipment_name.' indicates NoShipping for this method, specified by rule "'.$method->rule_name.'" ('.$match['rule']->rulestring.').');
return FALSE;
} else {
return TRUE;
}
}
vmdebug('checkConditions '.$method->shipment_name.' does not fulfill all conditions, no rule matches');
return FALSE;
}
/**
* @param VirtueMartCart $cart
* @param $method
* @param $cart_prices
* @return int
*/
function getCosts (VirtueMartCart $cart, $method, $cart_prices) {
if (!isset($this->rules[$method->virtuemart_shipmentmethod_id]))
$this->parseMethodRules($method);
$match = $this->evaluateMethodRules ($cart, $method, $cart_prices);
if ($match) {
$r = $match["rule"];
vmdebug('Rule ' . $match["rule_name"] . ' ('.$r->rulestring.') matched.');
$method->tax_id = $r->tax_id;
// TODO: Shall we include the name of the modifiers, too?
$method->rule_name = $match["rule_name"];
// Final shipping costs are calculated as:
// Shipping*ExtraShippingMultiplier + ExtraShippingCharge
// with possibly multiple modifiers
$method->cost = $r->getShippingCosts();
foreach ($match['modifiers_multiply'] as $modifier) {
$method->cost *= $modifier->getValue();
}
foreach ($match['modifiers_add'] as $modifier) {
$method->cost += $modifier->getValue();
}
$method->includes_tax = $r->includes_tax;
return $method->cost;
}
vmdebug('getCosts '.$method->name.' does not return shipping costs');
return 0;
}
/**
* update the plugin cart_prices
*
......@@ -469,7 +315,7 @@ class plgVmShipmentRules_Shipping_Base extends vmPSPlugin {
}
} else {
// BEGIN_RK_CHANGES: VM change in VM3!
if (is_array($calculator->_cartData)) { // VM2:
if (isset($calculator->_cartData) && is_array($calculator->_cartData)) { // VM2:
$taxrules = array_merge($calculator->_cartData['VatTax'],$calculator->_cartData['taxRulesBill']);
} else { // VM3:
$taxrules = array_merge($cart->cartData['VatTax'],$cart->cartData['taxRulesBill']);
......@@ -566,273 +412,6 @@ class plgVmShipmentRules_Shipping_Base extends vmPSPlugin {
}
protected function createMethodRule ($r, $countries, $tax) {
return new ShippingRule($this, $r, $countries, $tax);
}
private function parseMethodRule ($rulestring, $countries, $tax, &$method) {
$rules1 = preg_split("/(\r\n|\n|\r)/", $rulestring);
foreach ($rules1 as $r) {
// Ignore empty lines
if (empty($r)) continue;
$this->rules[$method->virtuemart_shipmentmethod_id][] = $this->createMethodRule ($r, $countries, $tax);
}
}
protected function parseMethodRules (&$method) {
if (!isset($this->rules[$method->virtuemart_shipmentmethod_id]))
$this->rules[$method->virtuemart_shipmentmethod_id] = array();
$this->parseMethodRule ($method->rules1, $method->countries1, $method->tax_id1, $method);
$this->parseMethodRule ($method->rules2, $method->countries2, $method->tax_id2, $method);
$this->parseMethodRule ($method->rules3, $method->countries3, $method->tax_id3, $method);
$this->parseMethodRule ($method->rules4, $method->countries4, $method->tax_id4, $method);
$this->parseMethodRule ($method->rules5, $method->countries5, $method->tax_id5, $method);
$this->parseMethodRule ($method->rules6, $method->countries6, $method->tax_id6, $method);
$this->parseMethodRule ($method->rules7, $method->countries7, $method->tax_id7, $method);
$this->parseMethodRule ($method->rules8, $method->countries8, $method->tax_id8, $method);
}
/** Functions to calculate all the different variables for the given cart and given (sub)set of products in the cart */
protected function getOrderArticles (VirtueMartCart $cart, $products) {
$articles = 0;
foreach ($products as $product) {
$articles += $product->quantity;
}
return $articles;
}
protected function getOrderProducts (VirtueMartCart $cart, $products) {
return count($products);
}
protected function getOrderDimensions (VirtueMartCart $cart, $products, $length_dimension) {
/* Cache the value in a static variable and calculate it only once! */
$dimensions=array(
'volume' => 0,
'maxvolume' => 0, 'minvolume' => 9999999999,
'maxlength' => 0, 'minlength' => 9999999999, 'totallength' => 0,
'maxwidth' => 0, 'minwidth' => 9999999999, 'totalwidth' => 0,
'maxheight' => 0, 'minheight' => 9999999999, 'totalheight' => 0,
'maxpackaging' => 0, 'minpackaging' => 9999999999, 'totalpackaging' => 0,
);
foreach ($products as $product) {
$l = ShopFunctions::convertDimensionUnit ($product->product_length, $product->product_lwh_uom, $length_dimension);
$w = ShopFunctions::convertDimensionUnit ($product->product_width, $product->product_lwh_uom, $length_dimension);
$h = ShopFunctions::convertDimensionUnit ($product->product_height, $product->product_lwh_uom, $length_dimension);
$volume = $l * $w * $h;
$dimensions['volume'] += $volume * $product->quantity;
$dimensions['maxvolume'] = max ($dimensions['maxvolume'], $volume);
$dimensions['minvolume'] = min ($dimensions['minvolume'], $volume);
$dimensions['totallength'] += $l * $product->quantity;
$dimensions['maxlength'] = max ($dimensions['maxlength'], $l);
$dimensions['minlength'] = min ($dimensions['minlength'], $l);
$dimensions['totalwidth'] += $w * $product->quantity;
$dimensions['maxwidth'] = max ($dimensions['maxwidth'], $w);
$dimensions['minwidth'] = min ($dimensions['minwidth'], $w);
$dimensions['totalheight'] += $h * $product->quantity;
$dimensions['maxheight'] = max ($dimensions['maxheight'], $h);
$dimensions['minheight'] = min ($dimensions['minheight'], $h);
$dimensions['totalpackaging'] += $product->product_packaging * $product->quantity;
$dimensions['maxpackaging'] = max ($dimensions['maxpackaging'], $product->product_packaging);
$dimensions['minpackaging'] = min ($dimensions['minpackaging'], $product->product_packaging);
}
return $dimensions;
}
protected function getOrderWeights (VirtueMartCart $cart, $products, $weight_unit) {
$dimensions=array(
'weight' => 0,
'maxweight' => 0, 'minweight' => 9999999999,
);
foreach ($products as $product) {
$w = ShopFunctions::convertWeigthUnit ($product->product_weight, $product->product_weight_uom, $weight_unit);
$dimensions['maxweight'] = max ($dimensions['maxweight'], $w);
$dimensions['minweight'] = min ($dimensions['minweight'], $w);
$dimensions['weight'] += $w * $product->quantity;
}
return $dimensions;
}
protected function getOrderListProperties (VirtueMartCart $cart, $products) {
$categories = array();
$vendors = array();
$skus = array();
$manufacturers = array();
foreach ($products as $product) {
$skus[] = $product->product_sku;
$categories = array_merge ($categories, $product->categories);
$vendors[] = $product->virtuemart_vendor_id;
if (is_array($product->virtuemart_manufacturer_id)) {
$manufacturers = array_merge($manufacturers, $product->virtuemart_manufacturer_id);
} elseif ($product->virtuemart_manufacturer_id) {
$manufacturers[] = $product->virtuemart_manufacturer_id;
}
}
$skus = array_unique($skus);
$vendors = array_unique($vendors);
$categories = array_unique($categories);
$manufacturers = array_unique($manufacturers);
return array ('skus'=>$skus,
'categories'=>$categories,
'vendors'=>$vendors,
'manufacturers'=>$manufacturers,
);
}
protected function getOrderCountryState (VirtueMartCart $cart, $address) {
$data = array (
'countryid' => 0, 'country' => '', 'country2' => '', 'country3' => '',
'stateid' => 0, 'state' => '', 'state2' => '', 'state3' => '',
);
$countriesModel = VmModel::getModel('country');
if (isset($address['virtuemart_country_id'])) {
$data['countryid'] = $address['virtuemart_country_id'];
// The following is a workaround to make sure the cache is invalidated
// because if some other extension meanwhile called $countriesModel->getCountries,
// the cache will be modified, but the model's id will not be changes, so the
// getData call will return the wrong cache.
$countriesModel->setId(0);
$countriesModel->setId($address['virtuemart_country_id']);
$country = $countriesModel->getData($address['virtuemart_country_id']);
if (!empty($country)) {
$data['country'] = $country->country_name;
$data['country2'] = $country->country_2_code;
$data['country3'] = $country->country_3_code;
}
}
$statesModel = VmModel::getModel('state');
if (isset($address['virtuemart_state_id'])) {
$data['stateid'] = $address['virtuemart_state_id'];
// The following is a workaround to make sure the cache is invalidated
// because if some other extension meanwhile called $countriesModel->getCountries,
// the cache will be modified, but the model's id will not be changes, so the
// getData call will return the wrong cache.
$statesModel->setId(0);
$statesModel->setId($address['virtuemart_state_id']);
$state = $statesModel->getData($address['virtuemart_state_id']);
if (!empty($state)) {
$data['state'] = $state->state_name;
$data['state2'] = $state->state_2_code;
$data['state3'] = $state->state_3_code;
}
}
return $data;
}
protected function getOrderAddress (VirtueMartCart $cart, $address) {
$zip = isset($address['zip'])?trim($address['zip']):'';
$data = array('zip'=>$zip,
'zip1'=>substr($zip,0,1),
'zip2'=>substr($zip,0,2),
'zip3'=>substr($zip,0,3),
'zip4'=>substr($zip,0,4),
'zip5'=>substr($zip,0,5),
'zip6'=>substr($zip,0,6),
'city'=>isset($address['city'])?trim($address['city']):'',
);
$data['company'] = isset($address['company'])?$address['company']:'';
$data['title'] = isset($address['title'])?$address['title']:'';
$data['first_name'] = isset($address['title'])?$address['title']:'';
$data['middle_name'] = isset($address['middle_name'])?$address['middle_name']:'';
$data['last_name'] = isset($address['last_name'])?$address['last_name']:'';
$data['address1'] = isset($address['address_1'])?$address['address_1']:'';
$data['address2'] = isset($address['address_2'])?$address['address_2']:'';
$data['city'] = isset($address['city'])?$address['city']:'';
$data['phone1'] = isset($address['phone_1'])?$address['phone_1']:'';
$data['phone2'] = isset($address['phone_2'])?$address['phone_2']:'';
$data['fax'] = isset($address['fax'])?$address['fax']:'';
$data['email'] = isset($address['email'])?$address['email']:'';
return $data;
}
protected function getOrderPrices (VirtueMartCart $cart, $products, $cart_prices) {
$data = array(
'amount' => 0,
'amountwithtax' => 0,
'amountwithouttax' => 0,
'baseprice' => 0,
'basepricewithtax' => 0,
'discountedpricewithouttax' => 0,
'salesprice' => 0,
'taxamount' => 0,
'salespricewithdiscount' => 0,
'discountamount' => 0,
'pricewithouttax' => 0,
);
if (!empty($cart_prices)) {
// get prices for the whole cart -> simply user the cart_prices
$data['amount'] = $cart_prices['salesPrice'];
$data['amountwithtax'] = $cart_prices['salesPrice'];
$data['amountwithouttax'] = $cart_prices['priceWithoutTax'];
$data['baseprice'] = $cart_prices['basePrice'];
$data['basepricewithtax'] = $cart_prices['basePriceWithTax'];
$data['discountedpricewithouttax'] = $cart_prices['discountedPriceWithoutTax'];
$data['salesprice'] = $cart_prices['salesPrice'];
$data['taxamount'] = $cart_prices['taxAmount'];
$data['salespricewithdiscount'] = $cart_prices['salesPriceWithDiscount'];
$data['discountamount'] = $cart_prices['discountAmount'];
$data['pricewithouttax'] = $cart_prices['priceWithoutTax'];
} else {
// Calculate the prices from the individual products!
// Possible problems are discounts on the order total
foreach ($products as $product) {
$data['amount'] += $product->quantity*$product->allPrices[$product->selectedPrice]['salesPrice'];
$data['amountwithtax'] += $product->quantity*$product->allPrices[$product->selectedPrice]['salesPrice'];
$data['amountwithouttax'] += $product->quantity*$product->allPrices[$product->selectedPrice]['priceWithoutTax'];
$data['baseprice'] += $product->quantity*$product->allPrices[$product->selectedPrice]['basePrice'];
$data['basepricewithtax'] += $product->quantity*$product->allPrices[$product->selectedPrice]['basePriceWithTax'];
$data['discountedpricewithouttax'] += $product->quantity*$product->allPrices[$product->selectedPrice]['discountedPriceWithoutTax'];
$data['salesprice'] += $product->quantity*$product->allPrices[$product->selectedPrice]['salesPrice'];
$data['taxamount'] += $product->quantity*$product->allPrices[$product->selectedPrice]['taxAmount'];
$data['salespricewithdiscount'] += $product->quantity*$product->allPrices[$product->selectedPrice]['salesPriceWithDiscount'];
$data['discountamount'] += $product->quantity*$product->allPrices[$product->selectedPrice]['discountAmount'];
$data['pricewithouttax'] += $product->quantity*$product->allPrices[$product->selectedPrice]['priceWithoutTax'];
}
}
return $data;
}
/** Allow child classes to add additional variables for the rules or modify existing one
*/
protected function addCustomCartValues (VirtueMartCart $cart, $products, $cart_prices, &$values) {
}
public function getCartValues (VirtueMartCart $cart, $products, $method, $cart_prices) {
$address = (($cart->ST == 0 || $cart->STsameAsBT == 1) ? $cart->BT : $cart->ST);
$cartvals = array_merge (
array(
'articles'=>$this->getOrderArticles($cart, $products),
'products'=>$this->getOrderProducts($cart, $products),
),
// Add the prices, optionally calculated from the products subset of the cart
$this->getOrderPrices ($cart, $products, $cart_prices),
// Add 'skus', 'categories', 'vendors' variables:
$this->getOrderListProperties ($cart, $products),
// Add country / state variables:
$this->getOrderAddress ($cart, $address),
$this->getOrderCountryState ($cart, $address),
// Add Total/Min/Max weight and dimension variables:
$this->getOrderWeights ($cart, $products, $method->weight_unit),
$this->getOrderDimensions ($cart, $products, $method->length_unit)
);
// Let child classes update the $cartvals array, or add new variables
$this->addCustomCartValues($cart, $products, $cart_prices, $cartvals);
// Finally, call the triger of vmshipmentrules plugins to let them add/modify variables
JPluginHelper::importPlugin('vmshipmentrules');
JDispatcher::getInstance()->trigger('onVmShippingRulesGetCartValues',array(&$cartvals, $cart, $products, $method, $cart_prices));
return $cartvals;
}
/**
* Create the table for this plugin if it does not yet exist.
* This functions checks if the called plugin is active one.
......@@ -934,16 +513,14 @@ class plgVmShipmentRules_Shipping_Base extends vmPSPlugin {
if (isset($data['rules1'])) {
// Try to parse all rules (and spit out error) to inform the user. There is no other
// reason to parse the rules here, it's really only to trigger warnings/errors in case of a syntax error.
$method = new StdClass ();
$method->virtuemart_shipmentmethod_id = $data['virtuemart_shipmentmethod_id'];
$this->parseMethodRule ($data['rules1'], isset($data['countries1'])?$data['countries1']:array(), $data['tax_id1'], $method);
$this->parseMethodRule ($data['rules2'], isset($data['countries2'])?$data['countries2']:array(), $data['tax_id2'], $method);
$this->parseMethodRule ($data['rules3'], isset($data['countries3'])?$data['countries3']:array(), $data['tax_id3'], $method);
$this->parseMethodRule ($data['rules4'], isset($data['countries4'])?$data['countries4']:array(), $data['tax_id4'], $method);
$this->parseMethodRule ($data['rules5'], isset($data['countries5'])?$data['countries5']:array(), $data['tax_id5'], $method);
$this->parseMethodRule ($data['rules6'], isset($data['countries6'])?$data['countries6']:array(), $data['tax_id6'], $method);
$this->parseMethodRule ($data['rules7'], isset($data['countries7'])?$data['countries7']:array(), $data['tax_id7'], $method);
$this->parseMethodRule ($data['rules8'], isset($data['countries8'])?$data['countries8']:array(), $data['tax_id8'], $method);
$this->helper->parseRuleSyntax ($data['rules1'], isset($data['countries1'])?$data['countries1']:array(), $data['tax_id1']);
$this->helper->parseRuleSyntax ($data['rules2'], isset($data['countries2'])?$data['countries2']:array(), $data['tax_id2']);
$this->helper->parseRuleSyntax ($data['rules3'], isset($data['countries3'])?$data['countries3']:array(), $data['tax_id3']);
$this->helper->parseRuleSyntax ($data['rules4'], isset($data['countries4'])?$data['countries4']:array(), $data['tax_id4']);
$this->helper->parseRuleSyntax ($data['rules5'], isset($data['countries5'])?$data['countries5']:array(), $data['tax_id5']);
$this->helper->parseRuleSyntax ($data['rules6'], isset($data['countries6'])?$data['countries6']:array(), $data['tax_id6']);
$this->helper->parseRuleSyntax ($data['rules7'], isset($data['countries7'])?$data['countries7']:array(), $data['tax_id7']);
$this->helper->parseRuleSyntax ($data['rules8'], isset($data['countries8'])?$data['countries8']:array(), $data['tax_id8']);
}
$ret=$this->setOnTablePluginParams ($name, $id, $table);
return $ret;
......@@ -951,541 +528,4 @@ class plgVmShipmentRules_Shipping_Base extends vmPSPlugin {
}
if (class_exists ('ShippingRule')) {
return;
}
/** Filter the given array of products and return only those that belong to the categories, manufacturers,
* vendors or products given in the $filter_conditions. The $filter_conditions is an array of the form:
* array( 'skus'=>array(....), 'categories'=>array(1,2,3,42), 'manufacturers'=>array(77,78,83), 'vendors'=>array(1,2))
* Notice that giving an empty array for any of the keys means "no restriction" and is exactly the same
* as leaving out the enty altogether
*/
function filterProducts($products, $filter_conditions) {
$result = array();
foreach ($products as $p) {
// JFactory::getApplication()->enqueueMessage("<pre>Product: ".print_r($p,1)."</pre>", 'error');
if (!empty($filter_conditions['skus']) && !in_array($p->product_sku, $filter_conditions['skus']))
continue;
if (!empty($filter_conditions['categories']) && count(array_intersect($filter_conditions['categories'], $p->categories))==0)
continue;
if (!empty($filter_conditions['manufacturers']) && count(array_intersect($filter_conditions['manufacturers'], $p->virtuemart_manufacturer_id))==0)
continue;
if (!empty($filter_conditions['vendors']) && !in_array($p->virtuemart_vendor_id, $filter_conditions['vendors']))
continue;
$result[] = $p;
}
return $result;
}
class ShippingRule {
var $plugin = Null;
var $rulestring = '';
var $name = '';
var $ruletype = '';
var $evaluated = False;
var $match = False;
var $value = Null;
var $shipping = 0;
var $conditions = array();
var $countries = array();
var $tax_id = 0;
var $includes_tax = 0;
function __construct ($plugin, $rule, $countries, $tax_id) {
if (is_array($countries)) {
$this->countries = $countries;
} elseif (!empty($countries)) {
$this->countries[0] = $countries;
}
$this->tax_id = $tax_id;
$this->rulestring = $rule;
$this->parseRule($rule);
$this->plugin=$plugin;
}
protected function parseRule($rule) {
$ruleparts=explode(';', $rule);
foreach ($ruleparts as $p) {
$this->parseRulePart($p);
}
}
protected function handleAssignment ($var, $value, $rulepart) {
switch (strtolower($var)) {
case 'name': $this->name = $value; break;
case 'shipping': $this->shipping = $value; $this->includes_tax = False; $this->ruletype='shipping'; break;
case 'shippingwithtax': $this->shipping = $value; $this->includes_tax = True; $this->ruletype='shipping'; break;
case 'variable': // Variable=... is the same as Definition=...
case 'definition': $this->name = strtolower($value); $this->ruletype = 'definition'; break;
case 'value': $this->shipping = $value; $this->ruletype = 'definition'; break; // definition values are also stored in the shipping member!
case 'extrashippingcharge': $this->shipping = $value; $this->ruletype = 'modifiers_add'; break; // modifiers are also stored in the shipping member!
case 'extrashippingmultiplier': $this->shipping = $value; $this->ruletype = 'modifiers_multiply'; break; // modifiers are also stored in the shipping member!
case 'comment': break; // Completely ignore all comments!
case 'condition': $this->conditions[] = $value; break;
default: JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_UNKNOWN_VARIABLE', $var, $rulepart), 'error');
}
}
protected function tokenize_expression ($expression) {
// First, extract all strings, delimited by quotes, then all text operators
// (OR, AND, in; but make sure we don't capture parts of words, so we need to
// use lookbehind/lookahead patterns to exclude OR following another letter
// or followed by another letter) and then all arithmetic operators
$re = '/\s*("[^"]*"|\'[^\']*\'|<=|=>|>=|=<|<>|!=|==|<|=|>)\s*/i';
$atoms = preg_split($re, $expression, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
// JFactory::getApplication()->enqueueMessage("TOKENIZING '$expression' returns: <pre>".print_r($atoms,1)."</pre>", 'error');
return $atoms;
}
protected function parseRulePart($rulepart) {
/* In the basic version, we only split at the comparison operators and assume each term on the LHS and RHS is one variable or constant */
/* In the advanced version, all conditions and costs can be given as a full mathematical expression */
/* Both versions create an expression tree, which can be easily evaluated in evaluateTerm */
$rulepart = trim($rulepart);
if (empty($rulepart)) return;
// Special-case the name assignment, where we don't want to interpret the value as an arithmetic expression!
if (preg_match('/^\s*(name|variable|definition)\s*=\s*(["\']?)(.*)\2\s*$/i', $rulepart, $matches)) {
$this->handleAssignment ($matches[1], $matches[3], $rulepart);
return;
}
// Split at all operators:
$atoms = $this->tokenize_expression ($rulepart);
/* TODO: Starting from here, the advanced plugin is different! */
$operators = array('<', '<=', '=', '>', '>=', '=>', '=<', '<>', '!=', '==');
if (count($atoms)==1) {
$this->shipping = $this->parseShippingTerm($atoms[0]);
$this->ruletype = 'shipping';
} elseif ($atoms[1]=='=') {
$this->handleAssignment ($atoms[0], $atoms[2], $rulepart);
} else {
// Conditions, need at least three atoms!
while (count($atoms)>1) {
if (in_array ($atoms[1], $operators)) {
$this->conditions[] = array($atoms[1], $this->parseShippingTerm($atoms[0]), $this->parseShippingTerm($atoms[2]));
array_shift($atoms);
array_shift($atoms);
} else {
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_UNKNOWN_OPERATOR', $atoms[1], $rulepart), 'error');
$atoms = array();
}
}
}
}
protected function parseShippingTerm($expr) {
/* In the advanced version, shipping cost can be given as a full mathematical expression */
// If the shipping term starts with a double quote, it is a string, so don't turn it into lowercase.
// All other expressions need to be turned into lowercase, because variable names are case-insensitive!
if (substr($expr, 0, 1) === '"') {
return $expr;
} else {
return strtolower($expr);
}
}
protected function evaluateComparison ($terms, $vals) {
while (count($terms)>2) {
$res = false;
switch ($terms[1]) {
case '<': $res = ($terms[0] < $terms[2]); break;
case '<=':
case '=<': $res = ($terms[0] <= $terms[2]); break;
case '==': $res = is_equal($terms[0], $terms[2]); break;
case '!=':
case '<>': $res = ($terms[0] != $terms[2]); break;
case '>=':
case '=>': $res = ($terms[0] >= $terms[2]); break;
case '>': $res = ($terms[0] > $terms[2]); break;
case '~':
$l=min(strlen($terms[0]), strlen($terms[2]));
$res = (strncmp ($terms[0], $terms[2], $l) == 0);
break;
default:
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_UNKNOWN_OPERATOR', $terms[1], $this->rulestring), 'error');
$res = false;
}
if ($res==false) return false;
// Remove the first operand and the operator from the comparison:
array_shift($terms);
array_shift($terms);
}
if (count($terms)>1) {
// We do not have the correct number of terms for chained comparisons, i.e. two terms leftover instead of one!
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_ERROR', $this->rulestring), 'error');
return false;
}
// All conditions were fulfilled, so we can return true
return true;
}
protected function evaluateListFunction ($function, $args) {
# First make sure that all arguments are actually lists:
$allarrays = True;
foreach ($args as $a) {
$allarrays = $allarrays && is_array($a);
}
if (!$allarrays) {
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_LISTFUNCTION_ARGS', $function, $this->rulestring), 'error');
return false;
}
switch ($function) {
case "length": return count($args[0]); break;
case "union":
case "join": return call_user_func_array( "array_merge" , $args); break;
case "complement": return call_user_func_array( "array_diff" , $args); break;
case "intersection": return call_user_func_array( "array_intersect" , $args); break;
case "issubset": # Remove all of superset's elements to see if anything else is left:
return !array_diff($args[0], $args[1]); break;
case "contains": # Remove all of superset's elements to see if anything else is left:
# Notice the different argument order compared to issubset!
return !array_diff($args[1], $args[0]); break;
case "list_equal": return array_unique($args[0])==array_unique($args[1]); break;
default:
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_LISTFUNCTION_UNKNOWN', $function, $this->rulestring), 'error');
return false;
}
}
protected function evaluateListContainmentFunction ($function, $args) {
# First make sure that the first argument is a list:
if (!is_array($args[0])) {
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_LISTFUNCTION_CONTAIN_ARGS', $function, $this->rulestring), 'error');
return false;
}
// Extract the array from the args, the $args varialbe will now only contain the elements to be checked:
$array = array_shift($args);
switch ($function) {
case "contains_any": // return true if one of the $args is in the $array
foreach ($args as $a) {
if (in_array($a, $array))
return true;
}
return false;
case "contains_all": // return false if one of the $args is NOT in the $array
foreach ($args as $a) {
if (!in_array($a, $array))
return false;
}
return true;
case "contains_only": // return false if one of the $array elements is NOT in $args
foreach ($array as $a) {
if (!in_array($a, $args))
return false;
}
return true;
case "contains_none": // return false if one of the $args IS in the $array
foreach ($args as $a) {
if (in_array($a, $array))
return false;
}
return true;
default:
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_LISTFUNCTION_UNKNOWN', $function, $this->rulestring), 'error');
return false;
}
}
/** Evaluate the given expression $expr only for the products that match the filter given by the scoping
* function and the corresponding conditions */
protected function evaluateScoping($expr, $scoping, $conditionvals, $vals, $products, $cartvals_callback) {
// JFactory::getApplication()->enqueueMessage("<pre>Scoping, begin, scoping=$scoping, expression=".print_r($expr,1).", conditionvals=".print_r($conditionvals, 1)."</pre>", 'error');
if (count($conditionvals)<1)
return $this->evaluateTerm($expr, $vals, $products, $cartvals_callback);
$filterkeys = array(
"evaluate_for_categories" => 'categories',
"evaluate_for_products" => 'products',
"evaluate_for_vendors" => 'vendors',
"evaluate_for_manufacturers" => 'manufacturers'
);
$conditions = array();
if (isset($filterkeys[$scoping]))
$conditions[$filterkeys[$scoping]] = $conditionvals;
// Pass the conditions to the parent plugin class to filter the current list of products:
$filteredproducts = filterProducts($products, $conditions);
// We have been handed a callback function to calculate the cartvals for the filtered list of products, so use it:
$filteredvals = $cartvals_callback($filteredproducts);
return $this->evaluateTerm ($expr, $filteredvals, $filteredproducts, $cartvals_callback);
}
protected function evaluateFunction ($function, $args) {
$func = strtolower($function);
// Check if we have a custom function definition and use that if so.
// This is done first to allow plugins to override even built-in functions!
if (isset($this->plugin->custom_functions[$func])) {
vmDebug("Evaluating custom function $function, defined by a plugin");
return call_user_func($this->plugin->custom_functions[$func], $args, $this);
}
// Functions with no argument:
if (count($args) == 0) {
$dt = getdate();
switch ($func) {
case "second": return $dt['seconds']; break;
case "minute": return $dt['minutes']; break;
case "hour": return $dt['hours']; break;
case "day": return $dt['mday']; break;
case "weekday":return $dt['wday']; break;
case "month": return $dt['mon']; break;
case "year": return $dt['year']; break;
case "yearday":return $dt['yday']; break;
}
}
// Functions with exactly one argument:
if (count($args) == 1) {
switch ($func) {
case "round": return round($args[0]); break;
case "ceil": return ceil ($args[0]); break;
case "floor": return floor($args[0]); break;
case "abs": return abs($args[0]); break;
case "not": return !$args[0]; break;
case "print_r": return print_r($args[0],1); break;
}
}
if (count($args) == 2) {
switch ($func) {
case "digit": return substr($args[0], $args[1]-1, 1); break;
case "round": return round($args[0]/$args[1])*$args[1]; break;
case "ceil": return ceil($args[0]/$args[1])*$args[1]; break;
case "floor": return floor($args[0]/$args[1])*$args[1]; break;
}
}
if (count($args) == 3) {
switch ($func) {
case "substring": return substr($args[0], $args[1]-1, $args[2]); break;
}
}
// Functions with variable number of args
switch ($func) {
case "max":
return max($args);
case "min":
return min($args);
case "list":
case "array":
return $args;
// List functions:
case "length":
case "complement":
case "issubset":
case "contains":
case "union":
case "join":
case "intersection":
case "list_equal":
return $this->evaluateListFunction ($func, $args);
case "contains_any":
case "contains_all":
case "contains_only":
case "contains_none":
return $this->evaluateListContainmentFunction($func, $args);
}
// None of the built-in function
// No known function matches => print an error, return 0
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_FUNCTION', $function, $this->rulestring), 'error');
return 0;
}
protected function evaluateVariable ($expr, $vals) {
$varname = strtolower($expr);
if (array_key_exists(strtolower($expr), $vals)) {
return $vals[strtolower($expr)];
} elseif ($varname=='noshipping') {
return $varname;
} elseif ($varname=='values') {
return $vals;
} elseif ($varname=='values_debug') {
return print_r($vals,1);
} else {
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_VALUE', $expr, $this->rulestring), 'error');
return null;
}
}
protected function evaluateTerm ($expr, $vals, $products, $cartvals_callback) {
// The scoping functions need to be handled differently, because they first need to adjust the cart variables to the filtered product list
// before evaluating its first argument. So even though parsing the rules handles scoping functions like any other function, their
// evaluation is fundamentally different and is special-cased here:
$scoping_functions = array("evaluate_for_categories", "evaluate_for_products", "evaluate_for_vendors", "evaluate_for_manufacturers");
$is_scoping = is_array($expr) && ($expr[0]=="FUNCTION") && (count($expr)>1) && in_array($expr[1], $scoping_functions);
if (is_null($expr)) {
return $expr;
} elseif (is_numeric ($expr)) {
return $expr;
} elseif (is_string ($expr)) {
// Explicit strings are delimited by '...' or "..."
if (($expr[0]=='\'' || $expr[0]=='"') && ($expr[0]==substr($expr,-1)) ) {
return substr($expr,1,-1);
} else {
return $this->evaluateVariable($expr, $vals);
}
} elseif ($is_scoping) {
$op = array_shift($expr); // ignore the "FUNCTION"
$func = array_shift($expr); // The scoping function name
$expression = array_shift($expr); // The expression to be evaluated
$conditions = $expr; // the remaining $expr list now contains the conditions
return $this->evaluateScoping ($expression, $func, $conditions, $vals, $products, $cartvals_callback);
} elseif (is_array($expr)) {
// Operator
$op = array_shift($expr);
$args = array();
// First evaluate all operands and only after that apply the function / operator to the already evaluated arguments
$evaluate = true;
if ($op == "FUNCTION") {
$evaluate = false;
}
foreach ($expr as $e) {
$term = $evaluate ? ($this->evaluateTerm($e, $vals, $products, $cartvals_callback)) : $e;
if ($op == 'COMPARISON') {
// For comparisons, we only evaluate every other term (the operators are NOT evaluated!)
// The data format for comparisons is: array('COMPARISON', $operand1, '<', $operand2, '<=', ....)
$evaluate = !$evaluate;
}
if ($op == "FUNCTION") {
$evaluate = true;
}
if (is_null($term)) return null;
$args[] = $term;
}
$res = false;
// Finally apply the operaton to the evaluated argument values:
switch ($op) {
// Logical operators:
case 'OR': foreach ($args as $a) { $res = ($res || $a); }; break;
case '&&':
case 'AND': $res = true; foreach ($args as $a) { $res = ($res && $a); }; break;
case 'IN': $res = in_array($args[0], $args[1]); break;
// Comparisons:
case '<':
case '<=':
case '=<':
case '==':
case '!=':
case '<>':
case '>=':
case '=>':
case '>':
case '~':
$res = $this->evaluateComparison(array($args[0], $op, $args[1]), $vals); break;
case 'COMPARISON':
$res = $this->evaluateComparison($args, $vals); break;
// Unary operators:
case '.-': $res = -$args[0]; break;
case '.+': $res = $args[0]; break;
// Binary operators
case "+": $res = ($args[0] + $args[1]); break;
case "-": $res = ($args[0] - $args[1]); break;
case "*": $res = ($args[0] * $args[1]); break;
case "/": $res = ($args[0] / $args[1]); break;
case "%": $res = (fmod($args[0], $args[1])); break;
case "^": $res = ($args[0] ^ $args[1]); break;
// Functions:
case "FUNCTION": $func = array_shift($args); $res = $this->evaluateFunction($func, $args); break;
default: $res = false;
}
// JFactory::getApplication()->enqueueMessage("<pre>Result of ".print_r($expr,1)." is $res.</pre>", 'error');
return $res;
} else {
// Neither string nor numeric, nor operator...
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_VALUE', $expr, $this->rulestring), 'error');
return null;
}
}
protected function calculateShipping ($vals, $products, $cartvals_callback) {
return $this->evaluateTerm($this->shipping, $vals, $products, $cartvals_callback);
}
protected function evaluateRule (&$vals, $products, $cartvals_callback) {
if ($this->evaluated)
return; // Already evaluated
$this->evaluated = True;
$this->match = False; // Default, set it to True below if all conditions match...
// First, check the country, if any conditions are given:
if (count ($this->countries) > 0 && !in_array ($vals['countryid'], $this->countries)) {
// vmdebug('Rule::matches: Country check failed: countryid='.print_r($vals['countryid'],1).', countries are: '.print_r($this->countries,1).'...');
return;
}
foreach ($this->conditions as $c) {
// All conditions have to match!
$ret = $this->evaluateTerm($c, $vals, $products, $cartvals_callback);
if (is_null($ret) || (!$ret)) {
return;
}
}
// All conditions match
$this->match = True;
// Calculate the value (i.e. shipping cost or modifier)
$this->value = $this->calculateShipping($vals, $products, $cartvals_callback);
// Evaluate the rule name as a translatable string with variables inserted:
// Replace all {variable} tags in the name by the variables from $vals
$matches=array();
$name=JText::_($this->name);
preg_match_all('/{([A-Za-z0-9_]+)}/', $name, $matches);
foreach ($matches[1] as $m) {
$val = $this->evaluateVariable($m, $vals);
if ($val !== null) {
$name = str_replace("{".$m."}", $val, $name);
}
}
$this->rulename = $name;
}
function matches(&$vals, $products, $cartvals_callback) {
$this->evaluateRule($vals, $products, $cartvals_callback);
return $this->match;
}
function getType() {
return $this->ruletype;
}
function getRuleName() {
if (!$this->evaluated)
vmDebug('WARNING: getRuleName called without prior evaluation of the rule, e.g. by calling rule->matches(...)');
return $this->rulename;
}
function getValue() {
if (!$this->evaluated)
vmDebug('WARNING: getValue called without prior evaluation of the rule, e.g. by calling rule->matches(...)');
return $this->value;
}
function getShippingCosts() {
return $this->getValue();
}
function isNoShipping() {
// NoShipping is set, so if the rule matches, this method should not offer any shipping at all
return (is_string($this->shipping) && (strtolower($this->shipping)=="noshipping"));
}
}
// No closing tag
<?php
/**
* Shipping by Rules generic helper class (Joomla/VM-specific)
* Reinhold Kainhofer, Open Tools, office@open-tools.net
* @copyright (C) 2012-2015 - Reinhold Kainhofer
* @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
**/
// defined('_JEXEC') or die( 'Direct Access to ' . basename( __FILE__ ) . ' is not allowed.' ) ;
// if (!class_exists( 'VmConfig' ))
// require(JPATH_ROOT.DS.'administrator'.DS.'components'.DS.'com_virtuemart'.DS.'helpers'.DS.'config.php');
// VmConfig::loadConfig();
if (!class_exists( 'RulesShippingFramework' ))
require_once (dirname(__FILE__) . DS . 'library' . DS . 'rules_shipping_framework.php');
// $test=new asdfasdsf();
class RulesShippingFrameworkJoomla extends RulesShippingFramework {
function getCustomFunctions() {
// Let other plugins add custom functions!
// The onVmShippingRulesRegisterCustomFunctions() trigger is expected to return an array of the form:
// array ('functionname1' => 'function-to-be-called',
// 'functionname2' => array($classobject, 'memberfunc')),
// ...);
JPluginHelper::importPlugin('vmshipmentrules');
$dispatcher = JDispatcher::getInstance();
$custfuncdefs = $dispatcher->trigger('onVmShippingRulesRegisterCustomFunctions',array());
return array ();
}
public function printWarning($message) {
// Keep track of warning messages, so we don't print them twice:
global $printed_warnings;
if (!isset($printed_warnings))
$printed_warnings = array();
if (!in_array($message, $printed_warnings)) {
JFactory::getApplication()->enqueueMessage($message, 'error');
$printed_warnings[] = $message;
}
}
/**
* Functions to calculate the cart variables:
* - getOrderArticles($cart, $products)
* - getOrderProducts
* - getOrderDimensions
*/
/** Functions to calculate all the different variables for the given cart and given (sub)set of products in the cart */
protected function getOrderArticles ($cart, $products) {
$articles = 0;
foreach ($products as $product) {
$articles += $product->quantity;
}
return $articles;
}
protected function getOrderProducts ($cart, $products) {
return count($products);
}
protected function getOrderDimensions ($cart, $products, $length_dimension) {
/* Cache the value in a static variable and calculate it only once! */
$dimensions=array(
'volume' => 0,
'maxvolume' => 0, 'minvolume' => 9999999999,
'maxlength' => 0, 'minlength' => 9999999999, 'totallength' => 0,
'maxwidth' => 0, 'minwidth' => 9999999999, 'totalwidth' => 0,
'maxheight' => 0, 'minheight' => 9999999999, 'totalheight' => 0,
'maxpackaging' => 0, 'minpackaging' => 9999999999, 'totalpackaging' => 0,
);
foreach ($products as $product) {
$l = ShopFunctions::convertDimensionUnit ($product->product_length, $product->product_lwh_uom, $length_dimension);
$w = ShopFunctions::convertDimensionUnit ($product->product_width, $product->product_lwh_uom, $length_dimension);
$h = ShopFunctions::convertDimensionUnit ($product->product_height, $product->product_lwh_uom, $length_dimension);
$volume = $l * $w * $h;
$dimensions['volume'] += $volume * $product->quantity;
$dimensions['maxvolume'] = max ($dimensions['maxvolume'], $volume);
$dimensions['minvolume'] = min ($dimensions['minvolume'], $volume);
$dimensions['totallength'] += $l * $product->quantity;
$dimensions['maxlength'] = max ($dimensions['maxlength'], $l);
$dimensions['minlength'] = min ($dimensions['minlength'], $l);
$dimensions['totalwidth'] += $w * $product->quantity;
$dimensions['maxwidth'] = max ($dimensions['maxwidth'], $w);
$dimensions['minwidth'] = min ($dimensions['minwidth'], $w);
$dimensions['totalheight'] += $h * $product->quantity;
$dimensions['maxheight'] = max ($dimensions['maxheight'], $h);
$dimensions['minheight'] = min ($dimensions['minheight'], $h);
$dimensions['totalpackaging'] += $product->product_packaging * $product->quantity;
$dimensions['maxpackaging'] = max ($dimensions['maxpackaging'], $product->product_packaging);
$dimensions['minpackaging'] = min ($dimensions['minpackaging'], $product->product_packaging);
}
return $dimensions;
}
protected function getOrderWeights ($cart, $products, $weight_unit) {
$dimensions=array(
'weight' => 0,
'maxweight' => 0, 'minweight' => 9999999999,
);
foreach ($products as $product) {
$w = ShopFunctions::convertWeigthUnit ($product->product_weight, $product->product_weight_uom, $weight_unit);
$dimensions['maxweight'] = max ($dimensions['maxweight'], $w);
$dimensions['minweight'] = min ($dimensions['minweight'], $w);
$dimensions['weight'] += $w * $product->quantity;
}
return $dimensions;
}
protected function getOrderListProperties ($cart, $products) {
$categories = array();
$vendors = array();
$skus = array();
$manufacturers = array();
foreach ($products as $product) {
$skus[] = $product->product_sku;
$categories = array_merge ($categories, $product->categories);
$vendors[] = $product->virtuemart_vendor_id;
if (is_array($product->virtuemart_manufacturer_id)) {
$manufacturers = array_merge($manufacturers, $product->virtuemart_manufacturer_id);
} elseif ($product->virtuemart_manufacturer_id) {
$manufacturers[] = $product->virtuemart_manufacturer_id;
}
}
$skus = array_unique($skus);
$vendors = array_unique($vendors);
$categories = array_unique($categories);
$manufacturers = array_unique($manufacturers);
return array ('skus'=>$skus,
'categories'=>$categories,
'vendors'=>$vendors,
'manufacturers'=>$manufacturers,
);
}
protected function getOrderCountryState ($cart, $address) {
}
protected function getOrderAddress ($cart) {
$address = (($cart->ST == 0 || $cart->STsameAsBT == 1) ? $cart->BT : $cart->ST);
$zip = isset($address['zip'])?trim($address['zip']):'';
$data = array('zip'=>$zip,
'zip1'=>substr($zip,0,1),
'zip2'=>substr($zip,0,2),
'zip3'=>substr($zip,0,3),
'zip4'=>substr($zip,0,4),
'zip5'=>substr($zip,0,5),
'zip6'=>substr($zip,0,6),
'city'=>isset($address['city'])?trim($address['city']):'',
'countryid' => 0, 'country' => '', 'country2' => '', 'country3' => '',
'stateid' => 0, 'state' => '', 'state2' => '', 'state3' => '',
);
$data['company'] = isset($address['company'])?$address['company']:'';
$data['title'] = isset($address['title'])?$address['title']:'';
$data['first_name'] = isset($address['title'])?$address['title']:'';
$data['middle_name'] = isset($address['middle_name'])?$address['middle_name']:'';
$data['last_name'] = isset($address['last_name'])?$address['last_name']:'';
$data['address1'] = isset($address['address_1'])?$address['address_1']:'';
$data['address2'] = isset($address['address_2'])?$address['address_2']:'';
$data['city'] = isset($address['city'])?$address['city']:'';
$data['phone1'] = isset($address['phone_1'])?$address['phone_1']:'';
$data['phone2'] = isset($address['phone_2'])?$address['phone_2']:'';
$data['fax'] = isset($address['fax'])?$address['fax']:'';
$data['email'] = isset($address['email'])?$address['email']:'';
// Country and State variables:
$countriesModel = VmModel::getModel('country');
if (isset($address['virtuemart_country_id'])) {
$data['countryid'] = $address['virtuemart_country_id'];
// The following is a workaround to make sure the cache is invalidated
// because if some other extension meanwhile called $countriesModel->getCountries,
// the cache will be modified, but the model's id will not be changes, so the
// getData call will return the wrong cache.
$countriesModel->setId(0);
$countriesModel->setId($address['virtuemart_country_id']);
$country = $countriesModel->getData($address['virtuemart_country_id']);
if (!empty($country)) {
$data['country'] = $country->country_name;
$data['country2'] = $country->country_2_code;
$data['country3'] = $country->country_3_code;
}
}
$statesModel = VmModel::getModel('state');
if (isset($address['virtuemart_state_id'])) {
$data['stateid'] = $address['virtuemart_state_id'];
// The following is a workaround to make sure the cache is invalidated
// because if some other extension meanwhile called $countriesModel->getCountries,
// the cache will be modified, but the model's id will not be changes, so the
// getData call will return the wrong cache.
$statesModel->setId(0);
$statesModel->setId($address['virtuemart_state_id']);
$state = $statesModel->getData($address['virtuemart_state_id']);
if (!empty($state)) {
$data['state'] = $state->state_name;
$data['state2'] = $state->state_2_code;
$data['state3'] = $state->state_3_code;
}
}
return $data;
}
protected function getOrderPrices ($cart, $products, $cart_prices) {
$data = array(
'amount' => 0,
'amountwithtax' => 0,
'amountwithouttax' => 0,
'baseprice' => 0,
'basepricewithtax' => 0,
'discountedpricewithouttax' => 0,
'salesprice' => 0,
'taxamount' => 0,
'salespricewithdiscount' => 0,
'discountamount' => 0,
'pricewithouttax' => 0,
);
if (!empty($cart_prices)) {
// get prices for the whole cart -> simply user the cart_prices
$data['amount'] = $cart_prices['salesPrice'];
$data['amountwithtax'] = $cart_prices['salesPrice'];
$data['amountwithouttax'] = $cart_prices['priceWithoutTax'];
$data['baseprice'] = $cart_prices['basePrice'];
$data['basepricewithtax'] = $cart_prices['basePriceWithTax'];
$data['discountedpricewithouttax'] = $cart_prices['discountedPriceWithoutTax'];
$data['salesprice'] = $cart_prices['salesPrice'];
$data['taxamount'] = $cart_prices['taxAmount'];
$data['salespricewithdiscount'] = $cart_prices['salesPriceWithDiscount'];
$data['discountamount'] = $cart_prices['discountAmount'];
$data['pricewithouttax'] = $cart_prices['priceWithoutTax'];
} else {
// Calculate the prices from the individual products!
// Possible problems are discounts on the order total
foreach ($products as $product) {
$data['amount'] += $product->quantity*$product->allPrices[$product->selectedPrice]['salesPrice'];
$data['amountwithtax'] += $product->quantity*$product->allPrices[$product->selectedPrice]['salesPrice'];
$data['amountwithouttax'] += $product->quantity*$product->allPrices[$product->selectedPrice]['priceWithoutTax'];
$data['baseprice'] += $product->quantity*$product->allPrices[$product->selectedPrice]['basePrice'];
$data['basepricewithtax'] += $product->quantity*$product->allPrices[$product->selectedPrice]['basePriceWithTax'];
$data['discountedpricewithouttax'] += $product->quantity*$product->allPrices[$product->selectedPrice]['discountedPriceWithoutTax'];
$data['salesprice'] += $product->quantity*$product->allPrices[$product->selectedPrice]['salesPrice'];
$data['taxamount'] += $product->quantity*$product->allPrices[$product->selectedPrice]['taxAmount'];
$data['salespricewithdiscount'] += $product->quantity*$product->allPrices[$product->selectedPrice]['salesPriceWithDiscount'];
$data['discountamount'] += $product->quantity*$product->allPrices[$product->selectedPrice]['discountAmount'];
$data['pricewithouttax'] += $product->quantity*$product->allPrices[$product->selectedPrice]['priceWithoutTax'];
}
}
return $data;
}
/** Allow child classes to add additional variables for the rules or modify existing one
*/
protected function addCustomCartValues ($cart, $products, $cart_prices, &$values) {
}
protected function addPluginCartValues($cart, $products, $method, $cart_prices, &$values) {
// Finally, call the triger of vmshipmentrules plugins to let them add/modify variables
JPluginHelper::importPlugin('vmshipmentrules');
JDispatcher::getInstance()->trigger('onVmShippingRulesGetCartValues',array(&$cartvals, $cart, $products, $method, $cart_prices));
}
/** Filter the given array of products and return only those that belong to the categories, manufacturers,
* vendors or products given in the $filter_conditions. The $filter_conditions is an array of the form:
* array( 'skus'=>array(....), 'categories'=>array(1,2,3,42), 'manufacturers'=>array(77,78,83), 'vendors'=>array(1,2))
* Notice that giving an empty array for any of the keys means "no restriction" and is exactly the same
* as leaving out the entry altogether
*/
public function filterProducts($products, $filter_conditions) {
$result = array();
foreach ($products as $p) {
if (!empty($filter_conditions['skus']) && !in_array($p->product_sku, $filter_conditions['skus']))
continue;
if (!empty($filter_conditions['categories']) && count(array_intersect($filter_conditions['categories'], $p->categories))==0)
continue;
if (!empty($filter_conditions['manufacturers']) && count(array_intersect($filter_conditions['manufacturers'], $p->virtuemart_manufacturer_id))==0)
continue;
if (!empty($filter_conditions['vendors']) && !in_array($p->virtuemart_vendor_id, $filter_conditions['vendors']))
continue;
$result[] = $p;
}
return $result;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment