diff --git a/rules_shipping_framework.php b/rules_shipping_framework.php index 7ec0ecf8b3fb2875417e4a21eab193fee0527d33..0c5123cb64119fb01fd43121a8ecb71013cf63bb 100644 --- a/rules_shipping_framework.php +++ b/rules_shipping_framework.php @@ -51,11 +51,12 @@ function is_equal($a, $b) { class RulesShippingFramework { static $_version = "0.1"; - protected $_callbacks = array(); + 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(); - var $custom_functions = array (); + protected $custom_functions = array (); + protected $available_scopings = array(); function __construct() { // $this->registerCallback('addCustomCartValues', array($this, 'addCustomCartValues')); @@ -79,6 +80,24 @@ class RulesShippingFramework { $this->callbacks[$callback] = $func; } + /** + * Register all possible scopings to the framework in the form + * array("skus" => "products" , "products" => "products") + * This registers functions evaluate_for_skus and evaluate_for_products, + * which both filter products (they are identical). + */ + public function registerScopings($scopings) { + $this->available_scopings = $scopings; + } + + /** + * Get the list of all scopings the framework implementation claims to have + * implemented. + */ + public function getScopings() { + return $this->available_scopings; + } + public function readableString($string) { switch ($string) { case "OTSHIPMENT_RULES_CUSTOMFUNCTIONS_ALREADY_DEFINED": @@ -113,6 +132,8 @@ class RulesShippingFramework { return "Unknown operator '%s' in shipment rule '%s'"; case "OTSHIPMENT_RULES_UNKNOWN_TYPE": return "Unknown rule type '%s' encountered for rule '%s'"; + case "OTSHIPMENT_RULES_SCOPING_UNKNOWN": + return "Unknown scoping function 'evaluate_for_%s' encountered in rule '%s'"; case "OTSHIPMENT_RULES_UNKNOWN_VARIABLE": return "Unknown variable '%s' in rule '%s'"; default: @@ -146,6 +167,10 @@ class RulesShippingFramework { return array (); } + function getCustomFunctionDefinitions() { + return $this->custom_functions; + } + /** @tag system-specific * @function printWarning() * Print a warning in the system-specific way. @@ -182,22 +207,14 @@ class RulesShippingFramework { */ public 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->warning('OTSHIPMENT_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->warning('OTSHIPMENT_RULES_CUSTOMFUNCTIONS_ALREADY_DEFINED', $fname); - } else { - $this->debug("Defining custom function $fname"); - $this->custom_functions[strtolower($fname)] = $func; - } + // 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 ($custfuncdefs as $fname => $func) { + if (isset($this->custom_functions[$fname]) && $this->custom_functions[$fname]!=$custfuncs[$fname]) { + $this->warning('OTSHIPMENT_RULES_CUSTOMFUNCTIONS_ALREADY_DEFINED', $fname); + } else { + $this->debug("Defining custom function $fname"); + $this->custom_functions[strtolower($fname)] = $func; } } } @@ -266,10 +283,18 @@ class RulesShippingFramework { return array(); } + protected function getDebugVariables ($cart, $products, $method) { + + return array( + 'debug_cart'=> print_r($cart,1), + 'debug_products' => print_r($products, 1), + ); + } + /** * Extract information about non-numerical zip codes (UK and Canada) from the postal code */ - protected function getAddressZIP ($zip) { + public function getAddressZIP ($zip) { $values = array(); // Postal code Check for UK postal codes: Use regexp to determine if ZIP structure matches and also to extract the parts. @@ -309,11 +334,14 @@ class RulesShippingFramework { /** Allow child classes to add additional variables for the rules or modify existing one */ protected function addCustomCartValues ($cart, $products, $method, &$values) { + // Pass all args through to the callback, if it exists if (isset($this->callbacks['addCustomCartValues'])) { - return $this->callbacks['addCustomCartValues']($cart, $products, $method, $values); + return call_user_func_array($this->callbacks['addCustomCartValues'], array($cart, $products, $method, &$values)/*func_get_args()*/); } + return $values; } protected function addPluginCartValues($cart, $products, $method, &$values) { + return $values; } public function getCartValues ($cart, $products, $method) { @@ -328,7 +356,8 @@ class RulesShippingFramework { $this->getOrderAddress ($cart, $method), // Add Total/Min/Max weight and dimension variables: $this->getOrderWeights ($cart, $products, $method), - $this->getOrderDimensions ($cart, $products, $method) + $this->getOrderDimensions ($cart, $products, $method), + $this->getDebugVariables ($cart, $products, $method) ); // Let child classes update the $cartvals array, or add new variables $this->addCustomCartValues($cart, $products, $method, $cartvals); @@ -396,7 +425,7 @@ class RulesShippingFramework { } } // None of the rules matched, so return NULL, but keep the evaluated results; - $this->match[$id] = $result; + $this->match[$id] = NULL; return NULL; } @@ -424,7 +453,7 @@ class RulesShippingFramework { $this->parseMethodRules($method); // TODO: This needs to be redone sooner or later! $match = $this->evaluateMethodRules ($cart, $method); - if ($match && !is_null ($match['rule'])) { + if ($match && isset($match['rule']) && !is_null ($match['rule'])) { $this->setMethodCosts($method, $match, null); // 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) @@ -455,7 +484,7 @@ class RulesShippingFramework { if (!isset($this->rules[$id])) $this->parseMethodRules($method); $match = $this->evaluateMethodRules ($cart, $method); - if ($match) { + if ($match && isset($match['rule']) && !is_null ($match['rule'])) { if ($this->handleNoShipping($match, $method)) { return $results; } @@ -509,7 +538,8 @@ class RulesShippingFramework { protected function createMethodRule ($r, $countries, $ruleinfo) { if (isset($this->callbacks['initRule'])) { - return $this->callbacks['initRule']($this, $r, $countries, $ruleinfo); + return call_user_func_array($this->callbacks['initRule'], + array($this, $r, $countries, $ruleinfo)); } else { return new ShippingRule($this, $r, $countries, $ruleinfo); } @@ -528,7 +558,7 @@ class RulesShippingFramework { $rules1 = preg_split("/(\r\n|\n|\r)/", $rulestring); foreach ($rules1 as $r) { // Ignore empty lines - if (empty($r)) continue; + if (empty($r) || trim($r)=='') continue; $result[] = $this->createMethodRule ($r, $countries, $ruleinfo); } return $result; @@ -614,7 +644,7 @@ class ShippingRule { /* 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; + if (!isset($rulepart) || $rulepart==='') return; // Special-case the name assignment, where we don't want to interpret the value as an arithmetic expression! @@ -764,24 +794,31 @@ class ShippingRule { } } + protected function normalizeScoping($scoping) { + $scopings = $this->framework->getScopings(); +// $this->framework->warning("<pre>normalizing Scoping $scoping. Registered scopings are: ".print_r($scopings,1)."</pre>"); + if (isset($scopings[$scoping])) { + return $scopings[$scoping]; + } else { + 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); - - // TODO: Make this more general! - $filterkeys = array( - "evaluate_for_categories" => 'categories', - "evaluate_for_products" => 'products', - "evaluate_for_skus" => 'products', - "evaluate_for_vendors" => 'vendors', - "evaluate_for_manufacturers" => 'manufacturers', - ); - $conditions = array(); - if (isset($filterkeys[$scoping])) - $conditions[$filterkeys[$scoping]] = $conditionvals; +// $this->framework->warning("<pre>evaluating scoping $scoping of expression ".print_r($expr,1)." with conditions ".print_r($conditionvals,1)."</pre>"); + // Normalize aliases (e.g. 'skus' and 'products' usually indicate the same scoping + $normalizedScoping = $this->normalizeScoping($scoping); + if (!$normalizedScoping) { + $this->framework->warning('OTSHIPMENT_RULES_SCOPING_UNKNOWN', $scoping, $this->rulestring); + return false; + } else { + $conditions = array($normalizedScoping => $conditionvals); + } // Pass the conditions to the parent plugin class to filter the current list of products: $filteredproducts = $this->framework->filterProducts($products, $conditions); @@ -794,9 +831,10 @@ class ShippingRule { $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])) { + $customfunctions = $this->framework->getCustomFunctionDefinitions(); + if (isset($customfunctions[$func])) { $this->framework->debug("Evaluating custom function $function, defined by a plugin"); - return call_user_func_array($this->plugin->custom_functions[$func], $args, $this); + return call_user_func($customfunctions[$func], $args, $this); } // Functions with no argument: @@ -878,8 +916,11 @@ class ShippingRule { return $varname; } elseif ($varname=='values') { return $vals; - } elseif ($varname=='values_debug') { - return print_r($vals,1); + } elseif ($varname=='values_debug' || $varname=='debug_values') { + $tmpvals = $vals; + unset($tmpvals['debug_cart']); + unset($tmpvals['debug_products']); + return print_r($tmpvals,1); } else { $this->framework->warning('OTSHIPMENT_RULES_EVALUATE_UNKNOWN_VALUE', $expr, $this->rulestring); return null; @@ -890,8 +931,7 @@ class ShippingRule { // 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); + $is_scoping = is_array($expr) && ($expr[0]=="FUNCTION") && (count($expr)>1) && (substr($expr[1], 0, 13)==="evaluate_for_"); if (is_null($expr)) { return $expr; @@ -906,10 +946,14 @@ class ShippingRule { } } elseif ($is_scoping) { $op = array_shift($expr); // ignore the "FUNCTION" - $func = array_shift($expr); // The scoping function name + $scope = substr(array_shift($expr), 13); // The scoping function name with "evaluate_for_" cut off $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); + // the remaining $expr list now contains the conditions. Evaluate them one by one: + $conditions = array(); + foreach ($expr as $e) { + $conditions[] = $this->evaluateTerm($e, $vals, $products, $cartvals_callback); + } + return $this->evaluateScoping ($expression, $scope, $conditions, $vals, $products, $cartvals_callback); } elseif (is_array($expr)) { // Operator