Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / PHP

Easily implementing your own online shop

4.88/5 (23 votes)
31 Oct 2014CPOL8 min read 89K   4  
How to easily build an online shop with the Scavix PHP Web Development Framework.

Introduction  

This article describes how to create an online shop system with the Scavix Web Development Framework in PHP. All codes are hosted in the GitHub repository, so you can easily browse them there. You should read this basics article first to get the basics of the Scavix Web Development Framework. 

Audience    

Experienced software developers with solid PHP, JavaScript and SQL knowledge.  

The Task

"We need a shop, nothing complicated"  

We often hear that from our customers and we're pretty sure you know that too. Fortunately we can show you how to set up a basic shop system using the Scavix WebFramework in no time. This sample shows you the basics so that you can then add all extra stuff for the shop you need. This gives you the freedom to start with a very basic slim shop code and then to extend it with all the addional logic for your purpose.

Requirements to the online shop are nearly the same most of the time:

  • Products listing 
  • Product details page 
  • Each product has a title, tagline, description, price and an image.
  • Shopping basket
  • Simple administration 
  • Different payment providers

All of this can be done really quick and easy. Of course there's much more, but this is the heart of nearly every shop system. If these basics are implemented, all adaptations are kind of easy. 

Great shop systems (and there are loads of very good shop systems out there) can do all that but need to be adjusted for every customer. These adjustments can eat up many days while implementing a small lightweight specialized shop would have been implemented in the same or less time.  Additionally, the full-fledged-online shop systems out there have loads of functions that are often not needed, especially when you have a shop with only a few products. 

Well, we won't discuss the pros and cons of other shop systems here, but just start with the...  

Database scheme  

It's quite common so we'll let an ERM speak for us: 

Image 1

As you can see, we will start without user registration but only let customers enter their address details. We'll also skip the administrative tables (for admin users and stuff) and just rely on hard-coded credentials for the admin part for now.  

Of course that is a no-go for live shops, but as mentioned earlier we want to focus on the shop basics. 

The sample code over at GitHub contains a function that will ensure the database structure is present and filled with some sample data in a SQLite database. Note that the above image just shows the basic idea behind the DB, not the real structure, so please do not blame us for fields you'll find in the code but not in the image :).

Basic setup    

As always when working with the Scavix Web Development Framework, you will need three files: index.php, config.php, and a (in fact optional) .htaccess file.

  • index.php contains the database setup code mentioned above but nothing else you don't already know.
  • config.php sets up the Scavix Web Development Framework and
  • .htaccess is needed for nice URLs. 

All of this is described in detail over here: Ultra-Rapid PHP Application Development

Structure  

We will need three controllers: Products, Basket and Admin.

Of course if you would split up into other logical units you may do that too!

Anyway: there will be a product listing, product details pages, a shopping basket page and an administration. From the basket page customers may start the payment process that will require some more pages to collect customers data.  

Image 2 

The layout 

We wont spend much time to create a nice layout for our sample shop. Just a base class for the controllers and a few lines of CSS code will be enough for now: 

PHP
//controller/shopbase.class.php 
<?php
use ScavixWDF\Base\HtmlPage;

class ShopBase extends HtmlPage { /* no code needed here */ }
?> 
PHP
// controller/shopbase.tpl.php
<div id="page">
    <div id="navigation">
        <a href="<?=buildQuery('Products')?>">Products</a>
        <a href="<?=buildQuery('Basket')?>">Basket</a>
        <a href="<?=buildQuery('Admin')?>">Administration (normally hidden)</a>
    </div>
    <div id="content">
        <? foreach( $content as $c ) echo $c; ?>    
    </div>
</div> 
CSS
// res/shopbase.css
#page { font: 14px normal Verdana,Arial,serif; }
#page > div { width: 960px; margin: auto; }
#navigation { padding-bottom: 10px; border-bottom: 1px solid gray; }
#navigation a
{
	font-size: 18px; 
	font-weight: bold; 
	margin-right: 15px;
}

.product_overview
{
	clear: both;
	margin-top: 25px;
	border-bottom: 1px solid gray;
	height: 75px;
}

.product_overview img
{
	width: 50px;
	float: left;
	margin-right: 10px;
}

.product_overview div
{
	white-space: nowrap;
	overflow: hidden;
}

.product_overview .title { font-weight: bold; }
.product_overview a { float: right; }

.product_basket
{
	clear: both;
}

.product_basket img
{
	width: 30px;
	float: left;
	margin-right: 10px;
}

.basket_total
{
	clear: both;
	text-align: right;
	font-size: 14px; 
	font-weight: bold;
} 

So for now ShopBase is just a central base class so that all derived classes will inherit the same layout. Of course it's always a good idea to share common CSS and program logic using inheritance, so that's no work for the bin. 

See the dirty hard-coded navigation links? That will be another task for later. 

There could be links only shown to verified users or perhaps the basket link could be hidden when empty. But as mentioned earlier: we don't want to loose the focus.   

Products   

We will implement two pages here: A product listing and a product details page.  

PHP
// controller/products.class.php
<?php
use ScavixWDF\Base\Template;
use ScavixWDF\JQueryUI\uiMessage;

class Products extends ShopBase
{
    /**
     * Lists all products.
     * @attribute[RequestParam('error','string',false)]
     */
    function Index($error)
    {
        // display error message if given
        if( $error )
            $this->content(uiMessage::Error($error));
        
        // loop thru the products...
        $ds = model_datasource('system');
        foreach( $ds->Query('products')->orderBy('title') as $prod )
        {
            //... and use a template to represent each
            $this->content( Template::Make('product_overview') )
                ->set('title',$prod->title)
                ->set('tagline',$prod->tagline)
                // see config.php where we set up products images folder as resource folder
                ->set('image',resFile($prod->image))
                ->set('link',buildQuery('Products','Details',array('id'=>$prod->id)))
                ;
        }
    }
    
    /**
     * Shows product details
     * @attribute[RequestParam('id','int')]
     */
    function Details($id)
    {
        // check if product really exists
        $ds = model_datasource('system');
        $prod = $ds->Query('products')->eq('id',$id)->current();
        if( !$prod )
            redirect('Products','Index',array('error'=>'Product not found'));
        
        // create a template with product details
        $this->content( Template::Make('product_details') )
            ->set('title',$prod->title)
            ->set('description',$prod->body)
            // see config.php where we set up products images folder as resource folder
            ->set('image',resFile($prod->image))
            ->set('link',buildQuery('Basket','Add',array('id'=>$prod->id)))
            ;
    }
} 

No magic in there, pretty straight code: the 'Index' method loops through all the products in the database and displays each of them using the template 'product_overview'. If you are not familiar with this please see the WDF basics again.  

PHP
// templates/product_overview.tpl.php
<div class="product_overview">
    <img src="<?=$image?>" alt=""/>
    <div class="title"><?=$title?></div>
    <div class="tagline"><?=$tagline?></div>
    <a href="<?=$link?>">Details...</a>
</div>  

The 'Details' method uses a similar approach but does not loop, but load a single product. It also uses another template file.  

PHP
// templates/product_details.tpl.php
<div class="product_details">
    <img src="<?=$image?>" alt=""/>
    <div class="title"><?=$title?></div>
    <div class="description"><?=$description?></div>
    <a href="<?=$link?>">Add to basket</a>
    <a href="javascript:history.back()">back to listing</a>
</div>  

Well....that's it: we have implemented the complete products part. 

The products listing page: 

Image 3

And the product's details page: 

Image 4 

The Basket 

Like almost any other shop out there we want a shopping basket. Simple principle: customers add products to it and can then change the amount of each product in the basket. Decreasing the amount to zero will remove the product from the basket. Of course that could be better but....you already know that 'keep the focus' sentence :). 

So straight for the code again:  

PHP
// controller/basket.class.php
<?php
use ScavixWDF\Base\Template;
use ScavixWDF\JQueryUI\uiButton;
use ScavixWDF\JQueryUI\uiMessage;

class Basket extends ShopBase
{
    /**
     * Lists all items in the basket.
     * @attribute[RequestParam('error','string',false)]
     */
    function Index($error)
    {
        // display any given error message
        if( $error )
            $this->content(uiMessage::Error($error));
        
        // prepare basket variable
        if( !isset($_SESSION['basket']) )
            $_SESSION['basket'] = array();
        
        if( count($_SESSION['basket']) == 0 )
            $this->content(uiMessage::Hint('Basket is empty'));
        else
        {
            // list all items in the basket ...
            $ds = model_datasource('system');
            $price_total = 0;
            foreach( $_SESSION['basket'] as $id=>$amount )
            {
                $prod = $ds->Query('products')->eq('id',$id)->current();
                
                //... each using a template
                $this->content( Template::Make('product_basket') )
                    ->set('title',$prod->title)
                    ->set('amount',$amount)
                    ->set('price',$prod->price)
                    // see config.php where we set up
                    // products images folder as resource folder
                    ->set('image',resFile($prod->image))
                    ->set('add',buildQuery('Basket','Add',array('id'=>$prod->id)))
                    ->set('remove',buildQuery('Basket','Remove',array('id'=>$prod->id)))
                    ;
                $price_total += $amount * $prod->price;
            }
            // display total price and the button to go on
            $this->content("<div 
              class='basket_total'>Total price: $price_total</div>");
            $this->content( uiButton::Make("Buy now") )->onclick = 
              "location.href = '".buildQuery('Basket','BuyNow')."'";
        }
    }
    
    /**
     * Adds a product to the basket.
     * @attribute[RequestParam('id','int')]
     */
    function Add($id)
    {
        // check if the product exists
        $ds = model_datasource('system');
        $prod = $ds->Query('products')->eq('id',$id)->current();
        if( !$prod )
            redirect('Basket','Index',array('error'=>'Product not found'));

        // increase the counter for this product
        if( !isset($_SESSION['basket'][$id]) )
            $_SESSION['basket'][$id] = 0;
        $_SESSION['basket'][$id]++;
        redirect('Basket','Index');
    }
    
    /**
     * Removes an item from the basket.
     * @attribute[RequestParam('id','int')]
     */
    function Remove($id)
    {
        // check if the product exists
        $ds = model_datasource('system');
        $prod = $ds->Query('products')->eq('id',$id)->current();
        if( !$prod )
            redirect('Basket','Index',array('error'=>'Product not found'));
        
        // decrease the counter for this product
        if( isset($_SESSION['basket'][$id]) )
            $_SESSION['basket'][$id]--;
        // and unset if no more items left
        if( $_SESSION['basket'][$id] == 0 )
            unset($_SESSION['basket'][$id]);
        redirect('Basket','Index');
    }

    /* full code: https://github.com/ScavixSoftware/WebFramework/blob/master/web/sample_shop/controller/basket.class.php */
} 

Again the 'Index' method provides a listing, this time the source is a SESSION variable that contain key-value pairs of product IDs and their quantity. 

The 'Add' and 'Remove' methods increase and decrease these quantities of the product in the basket.

The rest of the above code is checking if the product exists and if variables are present.

Finally to mention: Again we use a special template for the order items' listing: 

PHP
// templates/product_basket.tpl.php
<div class="product_basket">
    <img src="<?=$image?>" alt=""/>
    <span class="title"><?=$title?></span>
    <span class="amount">Amount: <?=$amount?></span>
    <span class="amount">Price: <?=$price?></span>
    <span class="amount">Total: <?=$amount * $price?></span>
    <a href="<?=$add?>">add one more</a>
    <a href="<?=$remove?>">remove one</a>
</div> 

That's it for the listing/editing part of the basket. The result looks like this: 

Image 5

Checkout

Well that's actually the most interesting part when implementing a shop system: How to get the customer's money. The sample provides you with interfaces to PayPal and Gate2Shop (because we worked with them in the past) and can be easily extended to support other providers too. For the development stage there's a testing payment provider too. 

The basic workflow for order payment is:

  • Get the customer's address data 
  • Store it along with the basket data into the database
  • Start the checkout process for the selected payment provider
  • React on payment messages from the provider

Complicated? No:

// controller/basket.class.php
<?php
use ScavixWDF\Base\Template;
use ScavixWDF\JQueryUI\uiButton;
use ScavixWDF\JQueryUI\uiMessage;

class Basket extends ShopBase
{
    /* full code: https://github.com/ScavixSoftware/WebFramework/blob/master/web/sample_shop/controller/basket.class.php*/
    
    /**
     * Entrypoint for the checkout process.
     * 
     * Requests customers address details and asks for payment processor.
     */
    function BuyNow()
    {
        // displays the chechout form, which has all inputs for address on it
        $this->content( Template::Make('checkout_form') );
    }
    
    /**
     * Persists current basket to the database and starts checkout process.
     * @attribute[RequestParam('fname','string')]
     * @attribute[RequestParam('lname','string')]
     * @attribute[RequestParam('street','string')]
     * @attribute[RequestParam('zip','string')]
     * @attribute[RequestParam('city','string')]
     * @attribute[RequestParam('email','string')]
     * @attribute[RequestParam('provider','string')]
     */
    function StartCheckout($fname,$lname,$street,$zip,$city,$email,$provider)
    {
        if( !$fname || !$lname || !$street || !$zip || !$city || !$email )
            redirect('Basket','Index',array('error'=>'Missing some data'));
        
        // create a new customer. note that we do not check for existance or stuff.
        // this should be part of a real shop system!
        $cust = new SampleCustomer();
        $cust->fname = $fname;
        $cust->lname = $lname;
        $cust->street = $street;
        $cust->zip = $zip;
        $cust->city = $city;
        $cust->email = $email;
        $cust->price_total = 0;
        $cust->Save();

        // create a new order and assign the customer (from above)
        $order = new SampleShopOrder();
        $order->customer_id = $cust->id;
        $order->created = 'now()';
        $order->Save();
        
        // now loop thru the basket-items and add them to the order...
        $ds = model_datasource('system');
        foreach( $_SESSION['basket'] as $id=>$amount )
        {
            //... by creating a dataset for each item
            $prod = $ds->Query('products')->eq('id',$id)->current();
            $item = new SampleShopOrderItem();
            $item->order_id = $order->id;
            $item->price = $prod->price;
            $item->amount = $amount;
            $item->title = $prod->title;
            $item->tagline = $prod->tagline;
            $item->body = $prod->body;
            $item->Save();
            
            $order->price_total += $amount * $prod->price;
        }
        // save the order again to persist the total amount
        $order->Save();
        $_SESSION['basket'] = array();
        
        // finally start the checkout process using the given payment provider
        log_debug("Handing control over to payment provider '$provider'");
        $p = new $provider();
        $p->StartCheckout($order,buildQuery('Basket','PostPayment'));
    }
    
    /**
     * This is the return URL for the payment provider.
     * Will be called when payment raches a final state, so control is handed over to our 
     * app again from the payment processor.
     */
    function PostPayment()
    {
        // we just display the $_REQUEST data for now.
        // in fact this is the point where some processing
        // should take place: send email to the team,
        // that prepares the items for shipping, send email(s) to customer,...
        log_debug("PostPayment",$_REQUEST);
        $this->content("<h1>Payment processed</h1>");
        $this->content("Provider returned this data:<br/>" + 
            "<pre>".render_var($_REQUEST)."</pre>");
    }
    
    /**
     * This is a special handler method for PayPal.
     * It will be called asynchronously from PayPal
     * backend so user will never see results of it.
     * Just here to update the database when payments
     * are ready or refunded or whatever.
     * See https://www.paypal.com/ipn for details
     * but in fact WebFramework will handle this for you.
     * Just needs this entry point for the callback.
     * @attribute[RequestParam('provider','string')]
     */
    function Notification($provider)
    {
        log_debug("Notification",$_REQUEST);
        $provider = new $provider();
        if( $provider->HandleIPN($_REQUEST) )
            die("OK");
        die("ERR");
    }
} 

That code creates a form asking for the customers address data and which payment provider he wants to use: 

Image 6

So far, so good. If you click 'Buy now' (with the Testing provider, as the others are missing some config) you will see nothing spectacular, but an order will be created.

Image 7 

To make it work in this simple manner the SampleShopOrder class must implement the IShopOrder interface. It provides some simple methods that allows generic handling of the payment process: 

// model/sampleshoporder.class.php
<?php
use ScavixWDF\Model\Model;
use ScavixWDF\Payment\IShopOrder;
use ScavixWDF\Payment\ShopOrderAddress;

/**
 * Represents an order in the database.
 * 
 * In fact nothing more than implementations for the inherited Model 
 * and the implemented IShopOrder interface.
 * See https://github.com/ScavixSoftware/WebFramework/wiki/classes_modules_payment#wiki-1c67f96d00c3c22f1ab9002cd0e3acbb
 * More logic would go into the Set* methods to handle different order states.
 * For our sample we just set the states in the DB.
 */
class SampleShopOrder extends Model implements IShopOrder
{
	const UNKNOWN  = 0;
	const PENDING  = 10;
	const PAID     = 20;
	const FAILED   = 30;
	const REFUNDED = 40;
	
	/**
	 * Returns the table name.
	 * See https://github.com/ScavixSoftware/WebFramework/wiki/classes_essentials_model_model.class#gettablename
	 */
	public function GetTableName() { return 'orders'; }

	/**
	 * Gets the orders address.
	 * @return ShopOrderAddress The order address
	 */
	public function GetAddress()
	{
		$res = new ShopOrderAddress();
		$res->Firstname = $this->fname;
		$res->Lastname = $this->lname;
		$res->Address1 = $this->street;
		$res->Zip = $this->zip;
		$res->City = $this->city;
		$res->Email = $this->email;
		return $res;
	}

	/**
	 * Gets the currency code.
	 * @return string A valid currency code
	 */
	public function GetCurrency() { return 'EUR'; }

	/**
	 * Gets the invoice ID.
	 * @return mixed Invoice identifier
	 */
	public function GetInvoiceId() { return "I".$this->id; }

	/**
	 * Gets the order culture code.
	 * 
	 * See <CultureInfo>
	 * @return string Valid culture code
	 */
	public function GetLocale() { return 'en-US'; }

        /**
	 * Return the total price incl. VAT (if VAT applies for the given country). 
	 * @param float $price The price without VAT.
	 * @return float Price including VAT (if VAT applies for the country).
	 */
	public function GetTotalPrice($price = false)
	{
		if( $price !== false )
			return $price * ( (1+$this->GetVatPercent()) / 100 );
		return $this->price_total * ( (1+$this->GetVatPercent()) / 100 );
	}

        /**
	 * Return the total VAT (if VAT applies for the given country). 
	 * @return float VAT in order currency
	 */
	public function GetTotalVat() { return $this->price_total * ($this->GetVatPercent()/100); }

        /**
	 * Return the total VAT percent (if VAT applies for the given country). 
	 * @return float VAT percent
	 */
	public function GetVatPercent() { return 19; }

	/**
	 * Returns all items.
	 * 
	 * @return array A list of <IShopOrderItem> objects
	 */
	public function ListItems() { return SampleShopOrderItem::Make()->eq('order_id',$this->id)->orderBy('id'); }

	/**
	 * Sets the currency
	 * @param string $currency_code A valid currency code
	 * @return void
	 */
	public function SetCurrency($currency_code) { /* we stay with EUR */ }

	/**
	 * Creates an instance from an order id.
	 * @return IShopOrder The new/loaded order <Model>
	 */
	public static function FromOrderId($order_id)
	{
		return SampleShopOrder::Make()->eq('id',$order_id)->current();
	}

	/**
	 * Called when the order has failed.
	 * 
	 * This is a callback from the payment processor. Will be called when there was an error in the payment process.
	 * This can be synchronous (when cutsomer aborts in then initial payment ui) or asynchronous when something goes wrong
	 * later in the payment processors processes.
	 * @param int $payment_provider_type Provider type identifier (<PaymentProvider>::PROCESSOR_PAYPAL, <PaymentProvider>::PROCESSOR_GATE2SHOP, ...)
	 * @param mixed $transaction_id Transaction identifier (from the payment provider)
	 * @param string $statusmsg An optional status message
	 * @return void
	 */
	public function SetFailed($payment_provider_type, $transaction_id, $statusmsg = false)
	{
		$this->status = self::FAILED;
		$this->updated = $this->deleted = 'now()';
		$this->Save();
	}

	/**
	 * Called when the order has been paid.
	 * 
	 * This is a callback from the payment processor. Will be called when the customer has paid the order.
	 * @param int $payment_provider_type Provider type identifier (<PaymentProvider>::PROCESSOR_PAYPAL, <PaymentProvider>::PROCESSOR_GATE2SHOP, ...)
	 * @param mixed $transaction_id Transaction identifier (from the payment provider)
	 * @param string $statusmsg An optional status message
	 * @return void
	 */
	public function SetPaid($payment_provider_type, $transaction_id, $statusmsg = false)
	{
		$this->status = self::PAID;
		$this->updated = $this->completed = 'now()';
		$this->Save();
	}

	/**
	 * Called when the order has reached pending state.
	 * 
	 * This is a callback from the payment processor. Will be called when the customer has paid the order but the
	 * payment has not yet been finished/approved by the provider.
	 * @param int $payment_provider_type Provider type identifier (<PaymentProvider>::PROCESSOR_PAYPAL, <PaymentProvider>::PROCESSOR_GATE2SHOP, ...)
	 * @param mixed $transaction_id Transaction identifier (from the payment provider)
	 * @param string $statusmsg An optional status message
	 * @return void
	 */
	public function SetPending($payment_provider_type, $transaction_id, $statusmsg = false)
	{
		$this->status = self::PENDING;
		$this->updated = 'now()';
		$this->Save();
	}

	/**
	 * Called when the order has been refunded.
	 * 
	 * This is a callback from the payment processor. Will be called when the payment was refunded for any reason.
	 * This can be reasons from the provider and/or from the customer (when he cancels the payment later).
	 * @param int $payment_provider_type Provider type identifier (<PaymentProvider>::PROCESSOR_PAYPAL, <PaymentProvider>::PROCESSOR_GATE2SHOP, ...)
	 * @param mixed $transaction_id Transaction identifier (from the payment provider)
	 * @param string $statusmsg An optional status message
	 * @return void
	 */
	public function SetRefunded($payment_provider_type, $transaction_id, $statusmsg = false)
	{
		$this->status = self::REFUNDED;
		$this->updated = $this->deleted = 'now()';
		$this->Save();
	}

	/**
	 * Checks if VAT needs to be paid.
	 * @return boolean true or false
	 */
	public function DoAddVat() { return true; /* Let's assume normal VAT customers for now */ }
} 

You will also need to create a class for order items, our is called SampleShopOrderItem and implements the IShopOrderItem interface.  

PHP
// model/sampleshoporderitem.class.php
<?php
use ScavixWDF\Model\Model;
use ScavixWDF\Payment\IShopOrderItem;

/**
 * Represents an order item in the database.
 * 
 * In fact nothing more than implementations for the inherited Model 
 * and the implemented IShopOrderItem interface.
 * See https://github.com/ScavixSoftware/WebFramework/wiki/classes_modules_payment#wiki-97745ff2e14aebb2225c7647a8a059bc
 */
class SampleShopOrderItem extends Model implements IShopOrderItem
{
	/**
	 * Returns the table name.
	 * See https://github.com/ScavixSoftware/WebFramework/wiki/classes_essentials_model_model.class#gettablename
	 */
	public function GetTableName() { return 'items'; }

	/**
	 * Gets the price per item converted into the requested currency.
	 * @param string $currency Currency code
	 * @return float The price per item converted into $currency
	 */
	public function GetAmount($currency) { return $this->price; }

	/**
	 * Gets the discount.
	 * @return float The discount
	 */
	public function GetDiscount() { return 0; }

	/**
	 * Gets the handling cost.
	 * @return float Cost for handling
	 */
	public function GetHandling() { return 0; }

	/**
	 * Gets the items name.
	 * @return string The item name
	 */
	public function GetName() { return $this->title; }

	/**
	 * Gets the quantity.
	 * @return float The quantity
	 */
	public function GetQuantity() { return $this->amount; }

	/**
	 * Gets the shipping cost.
	 * @return float Cost for shipping
	 */
	public function GetShipping() { return 0; }
}

Finally the payment module has to be configured:

// config.php 
<?php
// full code: https://github.com/ScavixSoftware/WebFramework/blob/master/web/sample_shop/config.php

// configure payment module with your IShopOrder class
$CONFIG["payment"]["order_model"] = 'SampleShopOrder';
// set up Gate2Shop if you want to use it
$CONFIG["payment"]["gate2shop"]["merchant_id"]      = '<your_merchant_id>';
$CONFIG["payment"]["gate2shop"]["merchant_site_id"] = '<your_merchant_site_id>';
$CONFIG["payment"]["gate2shop"]["secret_key"]       = '<your_secret_key>';
// set up PayPal if you want to use it
$CONFIG["payment"]["paypal"]["paypal_id"]      = '<your_paypal_id>';
$CONFIG["payment"]["paypal"]["notify_handler"] = array('Basket','Notification');

The Administration     

For a shop system you will need to be able to create products and have some kind of access to your customers data and their orders.  

Once you enter the administrative page it will ask you for credentials and (as mentioned above) they are hardcoded: use 'admin' as username and 'admin' as password.    

PHP
// controller/admin.class.php
<?php
use ScavixWDF\Base\AjaxAction;
use ScavixWDF\Base\AjaxResponse;
use ScavixWDF\Base\Template;
use ScavixWDF\Controls\Form\Form;
use ScavixWDF\JQueryUI\Dialog\uiDialog;
use ScavixWDF\JQueryUI\uiButton;
use ScavixWDF\JQueryUI\uiDatabaseTable;
use ScavixWDF\JQueryUI\uiMessage;

class Admin extends ShopBase
{
        /**
	 * Checks if aa admin has logged in and redirects to login if not.
	 */
	private function _login()
	{
		// check only the fact that somebody logged in
		if( $_SESSION['logged_in'] ) 
			return true;
		
		// redirect to login. this terminates the script execution.
		redirect('Admin','Login');
	}
	
	/**
	 * @attribute[RequestParam('username','string',false)]
	 * @attribute[RequestParam('password','string',false)]
	 */
	function Login($username,$password)
	{
		// if credentials are given, try to log in
		if( $username && $password )
		{
			// see config.php for credentials
			if( $username==cfg_get('admin','username') && $password==cfg_get('admin','password') )
			{
				$_SESSION['logged_in'] = true; // check only the fact that somebody logged in
				redirect('Admin');
			}
			$this->content(uiMessage::Error("Unknown username/passsword"));
		}
		// putting it together as control here. other ways would be to create a new class 
		// derived from Control or a Template (anonymous or with an own class)
		$form = $this->content(new Form());
		$form->content("Username:");
		$form->AddText('username', '');
		$form->content("<br/>Password:");
		$form->AddPassword('password', '');
		$form->AddSubmit("Login");
	}

        /* full code: https://github.com/ScavixSoftware/WebFramework/blob/master/web/sample_shop/controller/admin.class.php*/
}  

Each method in the Admin class calls the '_login' method, which redirects to 'Admin/Login' if no admin user has logged in. That method builds a login form without a template, just using plain Control syntax.


Really ugly in such raw development state, but works.    

Now we will start with the first part mentioned above: the product management.  

PHP
// controller/admin.class.php
<?php
use ScavixWDF\Base\AjaxAction;
use ScavixWDF\Base\AjaxResponse;
use ScavixWDF\Base\Template;
use ScavixWDF\Controls\Form\Form;
use ScavixWDF\JQueryUI\Dialog\uiDialog;
use ScavixWDF\JQueryUI\uiButton;
use ScavixWDF\JQueryUI\uiDatabaseTable;
use ScavixWDF\JQueryUI\uiMessage;

class Admin extends ShopBase
{
        /* full code: https://github.com/ScavixSoftware/WebFramework/blob/master/web/sample_shop/controller/admin.class.php */
     
        function Index()
	{
		$this->_login(); // require admin to be logged in
		
		// add products table and a button to create a new product
		$this->content("<h1>Products</h1>");
		$this->content(new uiDatabaseTable(model_datasource('system'),false,'products'))
			->AddPager(10)
			->AddRowAction('trash', 'Delete', $this, 'DelProduct');
		$this->content(uiButton::Make('Add product'))->onclick = AjaxAction::Post('Admin', 'AddProduct');
		
		// add orders table
		$this->content("<h1>Orders</h1>");
		$this->content(new uiDatabaseTable(model_datasource('system'),false,'orders'))
			->AddPager(10)
			->OrderBy = 'id DESC';
		
		// add customers table
		$this->content("<h1>Customers</h1>");
		$this->content(new uiDatabaseTable(model_datasource('system'),false,'customers'))
			->AddPager(10)
			->OrderBy = 'id DESC';
	}
	
	/**
	 * @attribute[RequestParam('title','string',false)]
	 * @attribute[RequestParam('tagline','string',false)]
	 * @attribute[RequestParam('body','text',false)]
	 * @attribute[RequestParam('price','double',false)]
	 */
	function AddProduct($title,$tagline,$body,$price)
	{
		$this->_login(); // require admin to be logged in
		
		// This is a quite simple condition: You MUST provide each of the variables
		if( $title && $tagline && $body && $price )
		{
			// store the uploaded image if present
			if( isset($_FILES['image']) && $_FILES['image']['name'] )
			{
				$i = 1; $image = __DIR__.'/../images/'.$_FILES['image']['name'];
				while( file_exists($image) )
					$image = __DIR__.'/../images/'.($i++).'_'.$_FILES['image']['name'];
				move_uploaded_file($_FILES['image']['tmp_name'], $image);
				$image = basename($image);
			}
			else 
				$image = '';
			
			// store the new product into the database
			$ds = model_datasource('system');
			$ds->ExecuteSql("INSERT INTO products(title,tagline,body,image,price)VALUES(?,?,?,?,?)",
				array($title,$tagline,$body,$image,$price));
			
			redirect('Admin');
		}
		// create a dialog and put a template on it.
		$dlg = new uiDialog('Add product',array('width'=>600,'height'=>450));
		$dlg->content( Template::Make('admin_product_add') );
		$dlg->AddButton('Add product', "$('#frm_add_product').submit()"); // frm_add_product is defined in the template
		$dlg->AddCloseButton("Cancel");
		return $dlg;
	}
	
	/**
	 * @attribute[RequestParam('table','string',false)]
	 * @attribute[RequestParam('action','string',false)]
	 * @attribute[RequestParam('model','array',false)]
	 * @attribute[RequestParam('row','string',false)]
	 */
	function DelProduct($table,$action,$model,$row)
	{
		$this->_login(); // require admin to be logged in
		
		// we use the ajax confirm features of the framework which require some translated string, so we set them up here
		// normally we would start the sysadmin and create some, but for this sample we ignore that.
		default_string('TITLE_DELPRODUCT','Delete Product');
		default_string('TXT_DELPRODUCT','Do you really want to remove this product? This cannot be undone!');
		if( !AjaxAction::IsConfirmed('DELPRODUCT') )
			return AjaxAction::Confirm('DELPRODUCT', 'Admin', 'DelProduct', array('model'=>$model));

		// load and delete the product dataset
		$ds = model_datasource('system');
		$prod = $ds->Query('products')->eq('id',$model['id'])->current();
		$prod->Delete();
		
		// delete the image too if present
		if( $prod->image )
		{
			$image = __DIR__.'/../images/'.$prod->image;
			if( file_exists($image) )
				unlink($image);
		}
		return AjaxResponse::Redirect('Admin');
	}
}

The 'Index' method creates a database table and a button to add another product. Simple but effective. The databasetable control allows us to add a row action, so we use that for a 'delete' trigger that will call 'DelProduct' when clicked.  

A click on the 'Add product' button will display a dialog with a form to enter all product data. Then (when dialog is accepted) the new product will be added to the database and the browser is redirected to refresh the product listing.  

Image 8

And guess what: that's it for the products admin basic part. Of course we are missing the 'edit' functionality and much more, but it shows the way we need to go for a full admin interface.  

Following that thought, we just display two more tables here: Orders and Customers. Well...just to show the basic idea: 

Image 9

What's next?

That's enough for a sample, but how do you go on? Well there are some 'standard' things to do:

  • Register at PayPal and/or Gate2Shop and update the shop configuration so that they become usable
  • Implement more code that make the payment workflow useful (send emails,...)
  • Extend the Admin-Interface to be able to manage products and orders 
  • .... 

Change-log   

  • 2013/05/8: Initial publishing 
  • 2013/05/23: Fixed broken links, updated code snippets to match the newest version
  • 2014/10/31: Added namespacing code

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)