Skip to content
gaiaops edited this page Nov 2, 2011 · 9 revisions

Souk framework

Definition

souk or suq (sk, shk) n.

A market, or part of a market, in an Arab city.

[Arabic sq, from Aramaic uq, street, market, from Akkadian squ, street, from sâqu, to be narrow; see yq in Semitic roots.]

Summary

A general purpose framework for creating online auctions. Written to be extensible, Souk can be tied to any inventory system you wish. It has an adapter for Stockpile, but is not required. Souk allows the seller to specify an item_id, quantity, price, bid minimum, expiration date, and bid increment for the listing, along with any other arbitrary attribute the application developer wishes to attach to the listing. An item can be for instant purchase, bid, or a combination of both.

In 30 seconds or less

Copy this file:

<?php
include '/path/to/gaia_core_php/autoload.php';
use Gaia\Souk;
use Gaia\DB;

DB\Connection::load( array('test'=> function(){ return new DB\Driver\PDO( 'sqlite:/tmp/souk.db'); }));
Gaia\Souk\Storage::attach( function ( Gaia\Souk\Iface $souk ){ return 'test';} );
Gaia\Souk\Storage::enableAutoSchema();

try {
    $app = 'test1';
    $seller_id = mt_rand(1, 1000000000);
    $buyer_id = mt_rand(1, 1000000000);
    $souk = new Souk( $app, $seller_id);
    $listing = $souk->auction( array('price'=>10, 'item_id'=>1, 'quantity'=>1, 'bid'=>2, 'step'=>1) );
    print "\nNew auction:\n";
    print_r( $listing->export() );
    $souk = new Souk( $app, $buyer_id);
    $listing = $souk->bid($listing->id, 10);
    print "\nAfter first bid:\n";
    print_r( $souk->get( $listing->id )->export() );
    $souk = new Souk( $app );
    $listing = $souk->close($listing->id);
    print "\nAfter closing:\n";
    print_r( $listing->export() );
    $souk = new Souk( $app, $seller_id );
    $listing = $souk->auction( array('item_id'=>1, 'quantity'=>1, 'price'=>10) );
    print "\nNew buy-only auction:\n";
    print_r( $listing->export() );
    $souk = new Souk( $app, $buyer_id);
    $listing = $souk->buy( $listing->id );
    print "\nAfter buying:\n";
    print_r( $listing->export() );
    $souk = new Souk( $app, $seller_id );
    $ids = array();
    $now = time();
    for( $i = 0; $i < 10; $i++){
        $listing = $souk->auction( array('item_id'=>1, 'price'=>10+$i, 'expires'=>$now + 86400 + $i) );
        $ids[] = $listing->id;
    }
    print "\ncreate a bunch of auctions and get them all at once\n";
    print_r( $souk->fetch( $ids ) );    
    print "\nSearch for the items we created\n";
    $search_options = array( 'seller'=>$seller_id,'closed'=>0, 'sort'=>'expires_soon', 'floor'=>11, 'ceiling'=>15);
    $ids =  $souk->search( $search_options );
    print_r( $souk->fetch( $ids ) );
    foreach( $ids as $id ) $souk->close( $id );
 
} catch( Exception $e ){
    print $e;
}

This script demonstrates all the essential features of souk. If you want to see the data it created, go to the souk database on your test db and look for tables test1_souk_listing_*. The data is sharded by week. Each listing is created under the week it is set to expire.

Instantiation

Souk was written to be extensible. The core of souk is pretty simple. Here is how you create a new Souk object:

$app = 'myapp';
$user_id = 1;
$souk = new Gaia\Souk($app, $user_id );

The user_id is an optional argument. You need the user id to invoke some methods, but not all. If no user_id was passed in, then those methods will throw exceptions. The methods that require a user id: auction, bid, and buy. Without a user_id, you can still search, fetch, and even close auctions.

Creating an Auction

There are no required parameters to create an auction, but at minimum you probably want to specify an item id for your listing:

$listing = $souk->auction( array('item_id'=>1));

Souk will provide reasonable default values for everything else. Maybe your application won't use item_id, though I can't yet imagine a scenario where that might be the case. Usually you will want to specify an item id so your listing can be searched.

Here is a list of all the parameters available when creating an auction:

  • item_id
  • quantity
  • price
  • bid
  • step

example:

$listing = $souk->auction( array('item_id'=>1, 'quantity'=>2, 'price'=>100, 'bid'=>10, 'step'=>5 ) );

We create a listing for an item of two items, with a buy-now price of 100, and an opening bid minimum of 10. Each bid thereafter must be 5 greater than that.

All attributes in souk are optional (item_id is not optional in Souk_Stockpile, but we'll get to that later), but if you specify any of these attribues above they should be integers. In general, the attributes probably should be integers. We want to keep the listings as lightweight as possible. If you need to store longer strings in here, it will work, but you are probably creating needless weight on your application. Discuss with [72] if you need this.

Souk also supports any arbitrary attributes you want to attach to your listing as well, as long as they can be cleanly encoded as json. These attributes will not be searchable, but you can display them.

Get a Listing

Souk returns an instance of the Souk_Listing object back to you after creating the row in the database. This object is returned from most methods of Souk, so get used to seeing it. The most important property you can access is the id field:

$id = $souk->auction( array('item_id'=>1) )->id;
$listing = $souk->get( $id );

Of course this example is silly. The auction method returns the listing object, and calling the get method returns the same information. But it illustrates how to look up a single row by id. Notice that the id number is a big integer with the leftmost 6 digits representing the year and week the auction expires. This is the shard that the row is in. The next 11 digits are the zero-padded representation of the row id. Souk uses this convention to be able to shard the data into many tables and still represent the id as a digit that is unique across the application.

Get multiple Listings

To get multiple entries at once, use the fetch method:

$listings = $souk->fetch( $ids );

It returns an array of listing objects keyed by their id.

Bid on an auction

To bid on an auction, just specify the id and the amount:

$listing = $souk->bid( $id, $amount );

If you fail to make the bid, an exception is thrown. You may not have matched the minimum bid, or someone could have outbid you. ou

Once the item has been closed at the end of the time period, if you are still the leading bidder, you become the buyer.

Buy a listing

Buying is even easier than bidding. Just pass in the id of the listing.

$listing = $souk->buy( $id );

The listing now shows you as a the buyer of the listing.

Search for listings

one of the most important parts of any marketplace is the search functionality. Souk allows you to filter and sort based on a number of different parameters. Let's go through an example:

$ids = $souk->search( array('sort'=>'expires_soon', 'floor'=>10, 'ceiling'=>100, 'seller'=>$seller_id ) );

Souk returns a list of up to 1000 ids. It doesn't paginate. It is your job to chunk up this list of ids and paginate the results into manageable result sets using array_slicing. for example:

$ids = $souk->search( array('sort'=>'expires_soon') );
$total = count( $ids );
$ids = array_slice( $ids, $offset, $limit );
$listings = $souk->fetch( $ids );

Since souk will return up to 1k ids, that should be more than enough to show. Gaia's market place is limited to total result sets of 500. Beyond that, paginated results become somewhat useless. 1k of ids will give you 20 pages worth of information with 50 listings per page. If the user can't find what they are looking for after clicking through 20 pages of matches, they should really narrow their search. It is your job to encourage them to do so if the total count approaches 1K.

Close a listing

When a listing passes its expiration time, it is no longer up for auction ... no bids can be submitted on it. But it won't be closed yet. Your application needs to manage this. Fortunately, with the job system this is pretty easy. Just schedule a job to run a minute after expiration time to run the close operation. We can also write a cron to clean up really old listings that were never handled by the job system.

$souk->close( $id );

The reason we don't automatically do all the work for you is because your application will need to send customized notifications to your users, and possibly do other cleanup tasks as an item is closed out. Here is a sample job:

    $r = $this->getRequest();
    $listing = MyApp::marketplace()->close($r->get('id'));
    return $listing->export();

Now when we create the auction, we can easily put the job in at the same time:

$listing = MyApp::marketplace( $user_id )->auction( array( 'item_id'=>1 ) );
$job = new Gaia\Job('/job/myapp.soukclose?id=' . $listing->id );
$job->queue = 'myapp_souk';
$job->start = $listing->expires + 60;
$job->store();

The cron way to handle this would be:

$souk = MyApp::marketplace();
$id = 0;
while( $list = $souk->pending( $age = 60, $limit = 1000, $id) ){
    foreach($list as $id ) $souk->close( $id );
}

This example will allow you to pull down pending job ids in sequential order from oldest to newest in chunks of 1000. It uses the current $id value to calculate where to resume when pulling down the next chunk of ids. This way you can batch through all the sharded tables and never repeat.

The job system is much more efficient because it can leverage distributed processing and avoids race conditions in trying to close bids. However, the cron may be easier to debug if there are some listings that are not being processed properly. You can use a combination of both safely, though you probably want to be careful not to run the cron too frequently.

Souk with Stockpile

So far, Souk doesn't enforce anything. If you buy something or win an auction, you are marked as the buyer, but no exchange takes place between seller and buyer. If your application uses Stockpile, you can do so pretty easily. First, you need to write a custom class that binds your stockpile factory methods to Souk. You want to bind the exact same factory methods to Souk as are used in the rest of your application. This is the stickiest part of building your app. Once you get over this hurdle, the rest is easy.

Here is an example binder:

class MyApp {
    public static function inventory( $user_id ){
        return new Gaia\Stockpile\Cacher( new Gaia\Stockpile\Hybrid( 'myapp', $user_id ) );
    }

    public static function currency( $user_id ){
        return new Gaia\Stockpile\Cacher( new Gaia\Stockpile\Tally( 'myapp_currency', $user_id ) );
    }
}

class MyApp_SoukBinder implements Souk\StockpileBinder_Iface {
    public function itemAccount( $user_id ){
       return MyApp::inventory( $user_id );
    }
    public function currencyAccount( $user_id){
       return MyApp::inventory( $user_id );
    }
    public function currencyId(){
       return 1;
    }
}

The itemAccount method lets souk know where to find the item to be sold. When the seller creates the auction, the item is removed from this location and placed into escrow for the duration of the auction. This prevents the seller from trying to do things like use or trade away the item somehow while the auction is still in progress. The currencyAccount is used to transfer funds from the buyer to the seller. When a user makes a bid, their funds are also placed into escrow until outbid or until the auction closes and payment is made. The currencyId method specifies what item_id to use to access currency in stockpile.

Souk is able to provide a reasonable escrow location for the items and currency by using standard conventions, but if you need better control of this you can override it by providing itemEscrow and currencyEscrow methods. See [72] for more details. I will provide more documentation on this later.

Instantiating the Souk_Stockpile

In its simplest form, it might look something like this:

$souk = new Souk('myapp', $user_id );
$binder = new MyApp_SoukBinder();
$souk = new Souk\Stockpile($souk, $binder );

Of course, you want to wrap this process up into a factory method, so you always do it consistently, and so down the line you can add caching and logging to souk. Let's just add it to the MyApp class earlier.

class MyApp {
     ....

     public static function market( $user_id = NULL ){
         return new Souk\Stockpile(new Souk('myapp', $user_id ), new MyApp_SoukBinder() );
    }
}


Then you can instantiate your souk object like this:

$souk = MyApp::market( $user_id );

From here it becomes easy to use the souk object as we do in the prior examples. The interface stays the same.

Souk Stockpile Quantities

Unless you specify a quantity, souk assumes the quantity is always 1. With Souk_Stockpile, we may be working with Tally, Serial, or Hybrid inventories. That means that some quantities can't be represented with a simple integer. No problem. Souk_Stockpile is compatible with any of these inventory types. You can pass in a quantity like so:

$quantity = array($serial);
MyApp::market( $user_id )->auction( array('quantity'=>$quantity, 'item_id'=>$item_id ) );

The code does the right thing and grabs the serial quantity along with any attached properties from the serial inventory in stockpile.