Have a new project in the pipeline?

Proposal Form

WooCommerce Rental Calculator

14th September, 2016

woocommerce

A Fully Bespoke Rental Price Calculator

WooCommerce is a hugely popular free plugin for WordPress which can provide an out-of-the-box, totally functional eShop with products of varying complexity, various shipping and payment methods, and a comprehensive checkout process. For a “standard” shop selling Products, ‘vanilla’ WooCommerce is amazing – it can get you up and running insanely fast, with little to none in the way of necessary development work. But when you want WooCommerce to do something it doesn’t do, it’s time to get down and dirty with some code. It should be noted that WooCommerce has a huge community producing and maintaining plugins which provide extra or changed functionality – for the most part, desired custom functionality can be achieved with one or two well-chosen plugins. But when plugins either cost too much, don’t meet your needs, or even overreach your needs and thus become a burden on the system, the only real option is to begin writing some bespoke code to produce your feature.

At Tidy Design, I do a lot of work with WordPress and WooCommerce, and more recently I’ve delved into pushing WooCommerce’s core functionality to produce a new shop for rental products. By default, the extent of WooCommerce’s options surrounding price is being able to set a sale price – but the requirements of this site included Products which have up to four price “brackets” which are used in a custom calculation on Product pages based on a customer’s input rental period. That’s quite a leap from WooCommerce’s default behaviour!

So let’s think of what exactly is required to get this working on a basic technical level:

1: Custom Product Data; the 4 price “brackets”. This must be configurable per-Product by administrators.
2: A “rental period” input on Product pages, representing the number of weeks the customer wants to rent the Product for.
3: A magic background calculator which can return a correct price for a given Product and rental period.
4: The ability to override the price for a Product in the cart, based on what was returned by the calculator.

Adding custom data to Products is a trivial two-stage (or in WordPress terms, a two-hook) process involving adding custom inputs to “edit Product” pages, and saving the values entered when the Administrator updates the post. I won’t cover that here because it’s a very “Googleable” subject – but in case you’re wondering, the first hook (adding custom inputs, specifically to the “Product Data” general tab) is

“woocommerce_product_options_general_product_data”

, and the second is

“woocommerce_process_product_meta”

– in which you’ll use WordPress’s built-in function “update_post_meta()” to write the values to the Product.

In the same way, adding a custom input to Product pages is trivial. The hook required depends on where you want the input to go. Mine is displayed above the normal Price output, so I used the hook

“woocommerce_single_product_summary”

to output some custom HTML.

The really interesting parts here are requirements three and four – the magic calculator, and overriding the WooCommerce price. Logically, the best option for the calculator was an AJAX function which is triggered by changes in the rental period input. WordPress has a hook-based method for adding AJAX functions that can be secured properly.

The below depicts a function which should receive a Product’s ID and the desired rental period, and return a calculated price.

function calculate_rental_price() {
		$price = 0;

		//get the product price data from the post
		$product_id = $_POST['rental_period'];
		$bracket_1 = floatval(get_post_meta($product_id, 'bracket_1', true)); //bracket 1 - for <1 week rentals
		$bracket_2 = floatval(get_post_meta($product_id, 'bracket_2', true)); //bracket 2 - for rental weeks 1 -> 4
		$bracket_2 = floatval(get_post_meta($product_id, 'bracket_3', true)); //bracket 3 - for rental weeks 5 + 6
		$bracket_4 = floatval(get_post_meta($product_id, 'bracket_4', true)); //bracket 4 - for rental weeks 7+

		$num_weeks = intval($_POST["num_weeks"]); //the input rental period

		if ($num_weeks === 0) {
			$price = $bracket_1;
		}
		else {
			for ($i = 1; $i < $num_weeks + 1; $i++) {
				if ($i <= 4) {
					$price += $bracket_2;
				}
				else if ($i <= 6) {
					$price += $bracket_3;
				}
				else if ($i >= 7) {
					$price += $bracket_4;
				}
			}
		}

		echo $price; //"return" the calculated price

		//write the calculated price to the PHP session
		session_start();
		$_SESSION['weeks'] = $num_weeks;
		$_SESSION['price'] = $price;

		die(); //exit
	}
	add_action('woocommerce_ajax_get_rental_price', 'calculate_rental_price');

So, at this stage, we have custom Product data depicting the various pricing brackets for different lengths of rental, we have a custom input on the Product page which allows a customer to request rental of any length in weeks, and we have a magic AJAX calculator which will return the correct price based on that input. The last stage is to tell WooCommerce “this is the price of the Product” when it is added to the cart.

By default, WooCommerce will take a Product’s regular or sale price as the price to display in the cart (and contribute to cart totals) – so we need a way to replace it. The first consideration might be to overwrite the regular and/or sale prices of the Product so that WooCommerce can just find the calculated price in the normal way. But consider what would happen here if two customers were ordering the same product at roughly the same time – which overwrite of the Product’s price would take precedence?

Luckily, when a Product is added to the cart in WooCommerce, a new “Cart Item” structure is created to represent it. This Cart Item is separate from the original Product object, and contains the price that will be displayed. We can overwrite this to any value we like – in our case, the calculated price.

But where does the calculated price come from? In the above AJAX function, I return the price to the page (where it is displayed by JavaScript where WooCommerce normally displays the price), but I also store it in the PHP session; a place to store variables which are relevant to the current user and which you want to “carry across” different pages.

It’s generally best practise to either not use the PHP session, or make its use as temporary as possible – so before I write the cart price of the product I transfer the values out of the session and into the cart item, utilising some custom array keys so as not to interfere with anything default.

This is achieved with the hook “woocommerce_add_cart_item_data” – which is run after the construction of a Cart Item, allowing the addition or modification of associated data.

	function transfer_rental_data_from_session_to_woocommerce_session($cart_item_data, $product_id) {
		session_start();
		$new_value = array();

		if (isset($_SESSION['weeks'])) {
			//write the value as text - "Three Day Express" for a 0-week rental period, and "x week(s)" for anything else.
			$option = $_SESSION['weeks'] == 0 ? 'Three Day Express' : ($_SESSION['weeks'] . ($_SESSION['weeks'] > 1 ? ' Weeks' : ' Week'));
			$new_value = array_merge($new_value, array('rental_period' => $option));
		}
		if (isset($_SESSION['price'])) {
			$option = $_SESSION['price'];
			$new_value = array_merge($new_value, array('custom_price' => $option));
		}

		//make sure to return the correct data - maintain the original cart item data, and only merge it with the new data if there is new data
		if (empty($new_value)) {
			return $cart_item_data;
		}
		else {
			if (empty($cart_item_data)) {
				return $new_value;
			}
			else {
				return array_merge($cart_item_data, $new_value);
			}
		}

		//get rid of the PHP session data - we don't need it any more
		session_unset();
		session_destroy();
	}
	add_filter('woocommerce_add_cart_item_data','transfer_rental_data_from_session_to_woocommerce_session', 1, 2);

So now, when a Product reaches the cart, its Cart Item has two new properties containing our custom data; ‘custom_price’ and ‘rental_period’. Data stored on the Cart Item allows for default WooCommerce functionality will continue to work – for example, the “undo” button after removing an item from the cart will restore the Cart Item, any custom data included. If the values were still hanging around in the PHP session, I’d have to do some clever logic to delete and recover it. As-is, no extra work is required to maintain the custom data.

Now for actually writing the cart price, using the hook “woocommerce_before_calculate_totals”.

	function overwrite_cart_price_for_rental_products($cart_object) {
		foreach ($cart_object->cart_contents as $cart_item_key => $cart_item) {
			if (array_key_exists('custom_price', $cart_item)) {
				$cart_item['data']->price = $cart_item['custom_price'];
			}
		}
	}
	add_action('woocommerce_before_calculate_totals', 'overwrite_cart_price_for_rental_products');

Quite simple, eh? All it does is take the previously-written custom price and copies it to the Cart Item’s price property. This is where WooCommerce looks to find the price to be displayed on the cart and to be considered in the cart totals.

So that’s it! All four technical requirements are met. We have our custom Product data, a rental period input on the Product page, a magic AJAX function and a mechanism for overwriting the cart price. In practice, this allows an Administrator to set up rental products which get cheaper the longer you rent them for, and it allows the customer to specify their desired rental period for every Product they order. Utilising the hook “woocommerce_cart_item_name”, I write out the rental period for each Product in the cart.

rental-period-cart

In essence, this is such a large functional change to WooCommerce that it could very well become its own plugin. I made careful considerations through all of the customisation (beyond what you see above) to ensure that only Products tagged as “Rental” adopt these changes, so this site can still set up and sell normal Products with set prices if desired.

The fact that I was able to implement this type of change goes to credit the incredible extensibility of WordPress and WooCommerce. Naturally, WooCommerce’s developers did not consider Products that work like these in their design process – but what they did do is make the system flexible enough that the remote developer has the opportunity to make them work.

It’s this extensibility and flexibility that have made me vow to never install a WooCommerce plugin. Anything that clients want is possible to achieve with a little bit of elbow grease and some well-placed hooks – though maybe one day I’ll produce my own set of plugins to re-use my custom functionality anywhere.

Happy customising!

Jonno

Web Design Posts

Recent Posts