I came up with this method using the strategy pattern, please let me know if you think this would work ok or if there are other ways to improve it. One thing that is nice is I can also create a separate unit test for each client's custom set of rules.
This is the base class. This will have all the available events where a rule is triggered along with default behavior. Right now I only have two rules. The first will look at the client record and determine if this client has chosen in their preferences to have all orders submitted 'on hold' so that they can review the order before having us pack/ship it.
Code: Select all
/**
* Abstract rules class
*
* Handles custom per client business rules on an event basis
*
* @author Jeff Dorsch
* @package FCP_Rules
* @abstract
*
*/
abstract class FCP_Rules_Abstract
{
/**
* Client object to retrieve client settings
*
* @var FCP_Client
* @access private
*/
private $client ;
/**
* Constructror
*
* @param FCP_Client $client
* @final
*/
final public function __construct(FCP_Client $client)
{
$this->client = $client ;
}
/**
* Called before a new order is inserted into the database.
*
* @param FCP_Order $order
*/
public function onBeforeOrderPlaced(FCP_Order $order)
{
if ($this->client->submitOrdersOnHold())
{
$order->setOnHold(true) ;
}
}
/**
* Called after an order has been marked as shipped
*
* @param FCP_Order $order
*/
public function onAfterOrderShips(FCP_Order $order)
{
// do stuff
}
}This is the default concrete rules object. I have this here so that I can implement any abstract methods I might add later.
Code: Select all
/**
* Default business rules object
*
* @author Jeff Dorsch
* @package FCP_Rules
* @final
*/
final class FCP_Rules_Default extends FCP_Rules_Abstract
{
}Here is a custom rules class to implement custom business rules specific to a client. The class ends in '5' which is the clientID.
Code: Select all
/**
* Rules for test company
*
* @author Jeff Dorsch
* @package FCP_Rules
*/
class FCP_Rules_5 extends FCP_Rules_Abstract
{
/**
* Called before a new order is inserted into the database.
*
* @param FCP_Order $order
*/
public function onBeforeOrderPlaced(FCP_Order $order)
{
parent::onBeforeOrderPlaced($order);
/// do special stuff with order
}
}Here is the factory class. It uses a static array to hold previously referenced rule objects so they are only created once. This is because every time a rule object is created a query will need to be done to pull the client record.
Custom rules will be in a rules directory named with the clientID. So the rules class above will be called '5.php'.
Code: Select all
/**
* Rules factory
*
* This class is used to instantiate a business rules object for a specific clientID
*
* Rules objects are cached for multiple calls
*
* @author Jeff Dorsch
* @package FCP_Rules
* @final
*
*/
final class FCP_Rules_Factory
{
/**
* Array of rule objects
*
* @var array
* @access private
* @static
*/
private static $ruleObjects = array();
/**
* Client mapper
*
* @var FCP_Client_Mapper
* @access private
*/
private $clientMapper ;
/**
* Constructor
*
* @param FCP_Client_Mapper $clientMapper
*/
public function __construct(FCP_Client_Mapper $clientMapper)
{
$this->clientMapper = $clientMapper ;
}
/**
* Create a rules object for the given client
*
* @param int $clientID
* @return FCP_Rules_Abstract
* @static
*/
public function create($clientID)
{
// Assert integer
if (!is_int($clientID)) throw new FCP_Rules_Exception('Integer expected') ;
// Create a new singleton for this client if none is set yet
if (!isset(self::$ruleObjects[$clientID]))
{
$client = $this->clientMapper->findByID($clientID) ;
// If the clientID is 123 the file name would be 'root/rules/123.php'
$incFile = APPLICATION_ROOT
.DIRECTORY_SEPARATOR.'rules'
.DIRECTORY_SEPARATOR.$clientID.'.php' ;
// If the file exists then include it and instantiate the rules object (named FCP_Rules_124 for clientID 123)
if (file_exists($incFile))
{
require_once($incFile) ;
$class = 'FCP_Rules_'.$clientID ;
self::$ruleObjects[$clientID] = new $class($client) ;
}
// Otherwise use the default object
else
{
self::$ruleObjects[$clientID] = new FCP_Rules_Default($client) ;
}
}
// Return the rule object for the given ID
return self::$ruleObjects[$clientID] ;
}
}Example usage. This would happen inside my OrderService object from the placeOrder() method. I don't intend to have the rules be called from anywhere outside of the service layer. A lot more happens inside the placeOrder() method, but this should give you an idea of where the rules come into play.
Code: Select all
// Create the objects I need
$clientMapper = new FCP_Client_Mapper($db) ;
$rulesFactory = new FCP_Rules_Factory($clientMapper) ;
$orderMapper = new FCP_Order_Mapper($db) ;
foreach ($orders as $order)
{
// Get the rules object
$rules = $rulesFactory->create($order->getClientID()) ;
// Run the event
$rules->onBeforeOrderPlaced($order);
// Inser the order
$orderMapper->insert($order) ;
}