In this tutorial, we run through the entire process of extending Shopp in the WordPress admin by writing a plugin that peaks into what shoppers are doing on your Shopp storefront.

Basket Snoop

basketsnoop

Basket Snoop

Introduction

When customizing and extending Shopp you are not limited to changes that affect the storefront (the publicly accessible portion) of your store – the shop’s admin side, or warehouse, is also highly extensible. In this tutorial we will look at the process of adding new admin pages to Shopp. We will take a specific problem and run through the entire process of creating a plugin that helps to solve that problem.

The Problem

It is not uncommon for shopping carts to be abandoned. For a variety of reasons a customer might change their mind about making a purchase – or some other problem may occur (such as a technical issue) which prevents them from completing the transaction. Having a better idea of how customers are behaving is valuable information and so for this tutorial we will look at the steps required to build a tool that allows the shop administrator to view the contents of shopping carts – both recently abandoned carts and those that are actively being filled by visitors to the site – from the comfort of the Shopp user interface.

Goals and Expectations

Our primary goal is to produce a functioning plugin that offers a solution to our stated problem and, in doing so, increase our understanding of the machinery that makes our Shopp-powered store tick. As part of this we will strive to do things the Shopp way. That means re-implementing the same good practices and leveraging as much of the existing Shopp codebase as possible – thereby reducing the amount of code we need to write and achieving deeper integration with Shopp at the same time. It is expected that readers of this tutorial will already be familiar with PHP and WordPress.  For those who wish to learn about developing plugins for WordPress first of all the following resources are recommended:

Planning

Rushing head-first into a new plugin can be fun and exhilarating, but it is generally a good idea to do a little forward planning first of all. In this tutorial we want to add a new capability to Shopp that will allow us to ‘snoop’ and see what lies inside visitors shopping carts. It is therefore vital that we know where information about the content of those shopping carts is stored. Just like WordPress itself, Shopp stores most data inside the database. Not all Shopp data is stored within the same core database tables as used by WordPress, however. Shopp uses a number of tables of its own and these can be identified by the shopp_ prefix (remembering of course that an additional prefix such as wp_ is normally present also):

  • shopp_address
  • shopp_asset
  • shopp_customer
  • shopp_index
  • shopp_meta
  • shopp_price
  • shopp_promo
  • shopp_purchase
  • shopp_purchased
  • shopp_shopping
  • shopp_summary

By looking over this list we can quickly identify a number of likely homes for shopping cart data and, indeed, it is the shopping table which we are particularly interested in on this occasion.

Shopping Data

To gather more information about the way shopping data is stored I decided to run some tests on a local installation that I use for experiments like this one. First of all I cleared out the existing data within the shopping table:

DELETE FROM wp_shopp_shopping WHERE 1;

I then opened a new tab in my browser and visited the storefront. With that done I used the following query to see what – if anything – had been recorded in the shopping table:

SELECT * FROM wp_shopp_shopping;

Sure enough a new row had been inserted, looking something like this:

Column Value
session q4auonb694j01aq00lc48v5u92
customer 0
ip 127.0.0.1
data O:8:"stdClass":7:{s:5:"Order";O:5:"Order":21:{s:8:"Customer";O:8:"Customer":25:{s:5:"login";b:0;s:4:"info";b:0;s:7:"newuser";b:0; …shortened for brevity
created 2012-01-24 10:21:54
modified 2012-01-24 10:21:54

As you can see above, the lion’s share of the new information is contained within the data column; although it may initially appear like a jumble of information those with an expert eye will recognise it as a serialized PHP object. This object encapsulates a wealth of data about the current shopping session. For the purposes of this tutorial we will focus on just two of the objects that are contained within this structure – the Order object and the Viewed array.

  • The Order object contains a variety of information such as shipping and billing data as well as a representation of the shopping cart itself.
  • The Viewed array is a record all the products that the visitor has looked at in the current session and so represents valuable intelligence to the store owner.

Now, if we return to the storefront and add an item to the cart we will then, upon inspecting the shopping object a second time, see that it has changed and now contains additional information.

The Order Object

If we look more closely at the Order object itself we will see that it is, in part, an amalgam of several different objects including:

  • Customer object
  • Shipping object
  • Billing object
  • Cart object

At this point there is not a lot of information in the first three. The Cart object however contains a new item – the product we just added. Continuing with this experiment then we might add a few more items to the cart and finally proceed to the checkout stage where we will provide our name and address. Let’s assume that we are using PayPal Standard as our payment option and click on the “Checkout with PayPal” button. As expected we are shown the Order Confirmation screen. Of course at this point we haven’t paid any money and, if we wish, we can abandon the cart – simply by navigating to a different website and forgetting about our prospective purchase. However, if we again inspect the Order object from the current shopping session we can see that the Customer, Shipping and Billing objects are now populated with information about our would-be customer. For example, here are some fields from the Billing object:

Field Name Value
name Pierre Trudeau
address 150B Yonge Street
city Toronto
state Ontario

If we pair this up with data from the Cart object and the Viewed array we can then see:

  • Who was visiting our store
  • What they attempted to purchase
  • What other items they looked at

Useful information indeed and, although the customer may have departed never to return, we can still access it for a limited period of time.

Putting the Data to Use

Now we have a much better idea of how shopping carts are stored we need some way of accessing them conveniently – using the MySQL command line or a tool such as phpMyAdmin for this purpose is at best awkward, particularly as the data is stored in serialized PHP format – not exactly human friendly. What we want to do is add a new admin screen and make it clear that this is connected to Shopp. Currently Shopp provides three menus within the WordPress dashboard:

  • Shopp
  • Catalog
  • Setup

Each of which has a number of sub-menu items. In this instance I have decided to add a new item to the Shopp menu – however it would be just as easy to work with either the Catalog or Setup menus.

Hooking the Shopp Admin Menus

First things first – let’s run through the steps required to create a new plugin.

  • This plugin is named “Basket Snoop” – so let’s create a new directory within wp-content/plugins and name it appropriately: “basketsnoop” is fine.
  • Now let’s create our main plugin file and call it snoop.php. To allow WordPress to identify our plugin we need to add some information to the top of this file (immediately after the opening <?php tag) so snoop.php should initially look something like this:
/*
Plugin Name: Basket Snoop
Version:     0.1
Author:      Barry Hughes
Author URI:  http://freshlybakedwebsites.net
Description: Provides a new admin panel that displays currently open/abandoned shopping carts.
*/

If we now head over to the WordPress dashboard and view the list of installed plugins we should see a brand new plugin called Basket Snoop, just waiting to be activated! Of course, as yet it will not actually do anything as we still need to write some code.

The first thing we are going to do is add our new admin page. Thankfully Shopp provides a hook for just this purpose – the shopp_admin_menus filter – so let’s go ahead and write add some code.

function basketsnoopinit(FlowController $admin) {
    // Require our controller class
    require dirname(__FILE__).'/includes/basketsnoop.php';

    // Only users with the Shopp Customers capability
    // should be able to see our page
    $admin->caps['basket-snoop'] = 'shopp_customers';

    // Add our page and specify the controller class
    $admin->addpage('basket-snoop',
        __('Basket Snoop', 'basket-snoop'),
        'BasketSnoop');

    return $admin;
}

// Hook into the Shopp admin menu process
add_filter('shopp_admin_menus', 'basketsnoopinit');

So what is actually happening here?

The process starts with our add_filter() call. What we are doing here is requesting that when the shopp_admin_menus event fires our function – basketsnoopinit() – be called. When this happens our function will be passed a reference to the AdminFlow object: it is this object that provides the facilities we need to add our own page.

The observant amongst you will notice that the first thing our function does is require a new file – we haven’t created this file yet but will shortly.

Next we assign a capability to our page. Since this plugin deals with information relating to (prospective) customers I have decided to assign a requirement for the shop_customers capability.

$admin->caps['basket-snoop'] = 'shopp_customers';

Then comes the code to add the page itself. A minimum of three parameters need to be passed: the internal name for the page, a label and the name of the class which will act as our controller.

$admin->addpage('basket-snoop',
    __('Basket Snoop', 'basket-snoop'),
    'BasketSnoop');

Controller

Now it is time to create our controller class. As a matter of personal preference I have placed the class file within a subdirectory, so my plugin directory now looks like this:

  • basketsnoop (dir)
  • snoop.php
  • includes (dir)

    • basketsnoop.php

If you are curious as to why I did not simply place the BasketSnoop class inside the same file as the preceding code the answer will become apparent when we create our new class. Let’s add some code to basketsnoop.php:

class BasketSnoop extends AdminController {

    public function admin() {
        echo 'Hello world!';
    }

}

Note that our class extends the AdminController (for those who aren’t of an object oriented mindset, in basic terms this means that BasketSnoop class will largely have access to the same functions and variables as the AdminController itself).

Since the AdminController definition is not loaded until later in the request we cannot place any references to it in our main plugin file – to do so would result in a fatal error.

If you haven’t already done so go right ahead and activate Basket Snoop from the Plugins menu. Next, hover over or expand the Shopp menu item and you should see a new entry: “Basket Snoop”. Click on this and, voila:

Hello world!

You should now see a WordPress admin page with the above message. Note that we do not need to instantiate a BasketSnoop object nor do we need to call the admin() method. This is all handled by Shopp.

To keep logic and presentation as separate as possible let’s create a new file in our includes directory called view.php. We’ll add more to it shortly, for now it can contain some basic mark-up:

<div class="wrap shopp"></div>

Let’s also change BasketSnoop::admin() slightly:

public function admin() {
    include dirname(__FILE__).'/view.php';
}

Save any changes and refresh or reload the Basket Snoop page.

Screenshot: new skeleton admin page

Admittedly there still isn’t much there – but even so it is already taking on the look and feel of any other WordPress (or indeed Shopp) page.

Even the Shopp icon is present next to the page title!

We can expand our view file later. Our next task is to retrieve information from the shopping table in the database.

Mining for Data

If you have developed for WordPress in the past then you are probably familiar with the WPDB class. It would be entirely possible for us to use the $wpdb global here – however Shopp also provides a class to help with database access and that is what we will use in this scenario.

Our plan is as follows:

  • Query for session IDs in the shopping table
  • Re-initialize each session
  • Grab the data we are interested in from each session
  • Display it!

To implement this we are going to have to expand our BasketSnoop class:

class BasketSnoop extends AdminController {

    protected $shoppingdata = array();

    public function admin () {
        // Load the shopping data and bring into scope
        $this->loadshopping();
        $data = $this->shoppingdata;

        // Display the data
        include dirname(__FILE__).'/view.php';
    }

    protected function loadshopping () {
        $db = DB::instance();
        $prefix = $db->table_prefix;

        // Run our query
        $results = DB::query("SELECT SQL_CALC_FOUND_ROWS
        session FROM {$prefix}shopp_shopping LIMIT 0, 10");

        $this->totalrows = $db->found; // Total number of rows

        // Extract the data
        foreach ($results as $result)
            $this->pulldata($result->session);

        // Clean up
        $db->results = array();
    }

    protected function pulldata ( $session ) {
        // Reload the relevant shopping session
        Shopping::resession($session);
        $shopping = Shopping::instance();

        $cart = $shopping->data->Order->Cart;
        $viewed = (array) $shopping->data->viewed;
        $customer = $this->customerdata($shopping->data->Order);

        // Add to the shopping data array
        $this->shoppingdata[$shopping->created] = array(
            'cart' => $cart,
            'viewed' => $viewed,
            'customer' => $customer
        );
    }

    protected function customerdata ( $order ) {
        // Pull available customer details
        $customer = array_merge(
        (array) $order->Shipping, // Least interest
        (array) $order->Billing, // Preferable
        (array) $order->Customer // Ideal
        );

        // Consolidate firstname/lastname
        if (array_key_exists('firstname', $customer) and
        !empty($customer['firstname']))
        $customer['name'] = $customer['firstname'].' '.$customer['lastname'];

        // Distill the array down to a limited number of fields
        $desired = array_flip(array(
            'name', 'address', 'xaddress', 'city', 'state',
            'country', 'postcode'));

        return array_intersect_key($customer, $desired);
    }
}

We have now moved much of the work into a new loadshopping() function. This grabs a reference to the Shopp DB (database) object and also obtains a copy of the table prefix for use in the query.

The query itself is straightforward:

DB::query("SELECT SQL_CALC_FOUND_ROWS session
FROM {$prefix}shopp_shopping LIMIT 0, 10");

Note the use of SQL_CALC_FOUND_ROWS. Although we are limiting the result set to just 10 rows this allows us to determine how many rows exist within the shopping table – without having to execute a new query.

For simplicity we have hardcoded the limit offset and row count values. In practice it would of course be desirable to make these variable in order to implement pagination in our view and, conveniently, Shopp further assists with this by making it easy to determine the result of SQL_CALC_FOUND_ROWS:

$this->totalrows = $db->found;

The query result itself is structured as an array. We can easily iterate across this array and extract the information. To keep our code clean and maintainable we have created an additional function called pulldata() to help with this.

What is of particular interest in the pulldata() function is that we can take the session ID (returned from our query) and bring the session back to life – thanks to the Shopping::resession() method.

// Load the order object from the relevant shopping session
Shopping::resession($session);
$shopping = Shopping::instance();

As discussed earlier in the tutorial our interest is in the Cart object and the Viewed array. We now quite simply extract these from the Shopping object and place them in our $shoppingdata array. Execution now returns to the admin() function and it brings the $shoppingdata into scope and pulls in the view.

View

Now we have our data, we just need to display it. Shopp itself does an excellent job of maintaining the WordPress look and feel. For the sake of clarity and simplicity we will not take things to the same level, but neither will we leave the end user bewildered by a display of cluttered data.

Without further ado let’s pad out our view file a little and create a skeleton table.

<div class="wrap shopp">
<div class="icon32"></div>
<h2></h2>
&nbsp;
<table class="widefat">
<thead>
<tr>
<th>Last updated</th>
<th>Cart items</th>
<th>Viewed items</th>
<th>Personal data</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Last updated</th>
<th>Cart items</th>
<th>Viewed items</th>
<th>Personal data</th>
</tr>
</tfoot>
<tbody>
<tr>
<td>Date here</td>
<td>Summary of cart items</td>
<td>Products viewed</td>
<td>Name, address if known</td>
</tr>
</tbody>
</table>
</div>

If you save your changes and view our Basket Snoop page you can see that even with this straightforward mark-up we have retained much of the WordPress look and feel.

Screenshot of our table layout

The next step is to remove our placeholders in the table body and render something useful.



<div class="wrap shopp">
    <div class="icon32"></div>
    <h2>Basket Snoop</h2>

    <table class="widefat">
        <thead>
        <tr>
            <th>Personal data</th>
            <th>Cart items</th>
            <th>Viewed items</th>
            <th>Reference</th>
        </tr>
        </thead>
        <tfoot>
        <tr>
            <th>Personal data</th>
            <th>Cart items</th>
            <th>Viewed items</th>
            <th>Reference</th>
        </tr>
        </tfoot>
        <tbody>
        <?php
            // Flip the result set
            $data = array_reverse($data, true);

            foreach ($data as $session => $shopper):
        ?>
        <tr>
            <td> <ul>
                <?php $customer = $shopper['customer']; ?>
                <?php if (empty($customer['name'])): ?>
                    <li> <em> Unknown </em></li>
                <?php else: ?>
                    <li> <?php echo esc_html($customer['name']) ?> </li>
                    <li> <a href="mailto:<?php echo esc_attr($customer->email) ?>">
                        <?php echo esc_html($customer['email']) ?>
                    </a> </li>
                    <li> <?php
                        echo esc_html($customer['address']).' ';
                        echo esc_html($customer['xaddress']).' <br />';
                        echo esc_html($customer['city']).' ';
                        echo esc_html($customer['postcode']).' ';
                        echo esc_html($customer['country']).' ';
                    ?> </li>
                <?php endif; ?>
            </ul> </td>
            <td> <ul>
            <?php foreach ($shopper['cart']->contents as $item): ?>
                <li> <?php echo esc_html($item->quantity) ?>x
                <strong><?php echo esc_html($item->name) ?></strong>
                <?php echo esc_html(money($item->unitprice)) ?> </li>
            <?php endforeach ?>
            </ul> </td>
            <td> <ul>
            <?php foreach ($shopper['viewed'] as $item): ?>
                <li> <?php
                    $product = shopp_product($item);
                    echo esc_html("$product->name (#$product->id)");
                ?></li>
            <?php endforeach ?>
            </ul> </td>
            <td> <ul>
                <li><?php echo esc_html($session)?></li>
                <li><em>Last change <?php echo esc_html($shopper['modified']) ?></em></li>
            </ul> </td>
        </tr>
        <?php endforeach ?>
        </tbody>
    </table>
</div>


Result

With this work done exactly what you will see depends of course on the shopping data currently stored in your WordPress database. However you can expect to see something like this:

Screenshot of the populated results table

Whilst there is a huge amount of room for improvement (see the following section on embellishments) we have successfully accomplished our goals and have extend the administrative capabilities of Shopp.

Embellishments

What’s missing and what would make this better? Though we have created a functioning plugin much could be done to extend its behavior.

  • Pagination to improve the user experience
  • Differentiation between abandoned and current carts
  • Thumbnail product images
  • Analysis of the last-to-be-viewed products
  • Expand the personal data column to show separate fields in relation to billing and shipping
  • Anything else you can think of!
Avatar of Barry

By

Barry Hughes is an independent web developer specializing in PHP and WordPress. When he isn't putting together plugins or baking up other tasty morsels of code he tries his best to update his blog at FreshlyBakedWebsites.net.

You must be logged in to post a comment.

© Ingenesis Limited. Shopp™ is a registered trademark of Ingenesis Limited.

Skip to toolbar