Site icon Leonid Mamchenkov

CakePHP : Building factories with models and behaviors

CakePHP is a wonderful framework.   Recently I proved it to myself once again (not that I need much of that proof anyway).  The problem that we had at work was a whole lot of code in once place and no obvious way of how to break that code into smaller, more manageable pieces.  MVC is a fine architecture, but it wasn’t obvious to me how to apply it to larger projects.

In our particular case, we happen to have several data types, which are very similar to each other, yet should be treated differently.  Two examples are:

  1. Client account registrations.   Our application supports different types of accounts and each type has its own processing, forms, validations, etc.  However, each type of account is still an account registration.
  2. Financial transactions.  Our clients can send us money via a number of payment methods – credit cards, PayPal, bank wires, etc.  Each type of the transaction has its own processing, forms, validations, etc.  However, each type of the transaction is still a financial transaction of money coming in.

Having a separate model for each type of account or for each type of transaction seems excessive.  There are differences between each type, but not enough to justify a separate model.  Having a single model though means that it’ll continue to grow with each and every difference that needs to be coded in.  Something like a class factory design pattern would solve the problem nicely, but the question is how to fit it into an existing MVC architecture.  Read the rest of this post for a demonstration.

CakePHP supports a notion of behaviors, which are a sort of modifier for a model.  Things that bloat a model can easily be moved out into a behavior.  So, for each type of our financial transaction we can create a separate behavior.  That would help, but how would we keep behaviors consistent and how can we share common code between them?  With a base model.  And furthermore, if we want to make sure that each behavior actually implements certain methods, we can force an interface constraint upon the behavior.  Enough of this blah blah blah.  Here comes the code.

First, let’s create a table for our transactions with some sample data.

DROP TABLE transactions;
CREATE TABLE transactions (
	id INTEGER UNIQUE AUTO_INCREMENT PRIMARY KEY,
	method CHAR(2) NOT NULL,
	amount DECIMAL(10,2) NOT NULL DEFAULT 0,
	currency CHAR(3) NOT NULL);
INSERT INTO transactions SET method='CC', amount=100, currency='USD';
INSERT INTO transactions SET method='BW', amount=1500, currency='GBP';
INSERT INTO transactions SET method='PP', amount=21.50, currency='EUR';

Needless to say, I am trying to simplify things, hence such a short table definition.  Method here is a two character description of the payment method (“CC” for credit card, “PP” for PayPal, “BW” for bank wire, etc).

Second, let’s create a couple of views, to get them out of the way. One to add a transaction (in app/views/transactions/add.ctp) with the following content:

<h1>Add transaction</h1>
<?php
echo $form->create('Transaction');
echo $form->input('method');
echo $form->input('amount');
echo $form->input('currency');
echo $form->end('Save Transaction');
?>

And another one to view all transactions (in app/views/transactions/index.ctp) with the following content:

<h1>Transactions</h1>
<a href="/transactions/add">Add transaction</a>
<table>
	<tr>
		<th>ID</th>
		<th>method</th>
		<th>amount</th>
		<th>currency</th>
	</tr>
	<?php
		foreach ($transactions as $transaction) {
			echo "<tr>";	
			echo "<td>{$transaction['Transaction']['id']}</td>";
			echo "<td>{$transaction['Transaction']['method']}</td>";
			echo "<td>{$transaction['Transaction']['amount']}</td>";
			echo "<td>{$transaction['Transaction']['currency']}</td>";
			echo "</tr>";	
		}
	?>
</table>

Nothing fancy, just good enough for us to see what we have in the database.

The next simplest thing is the transactions controller. Again, nothing fancy in here, just two actions for the above views. Here is the code for app/controllers/transactions_controller.php :

<?php
class TransactionsController extends AppController {
	var $name = 'Transactions';

	function index() {
		$this->set('transactions', $this->Transaction->find('all'));
	}

	function add() {
		if (!empty($this->data)) {
			if ($this->Transaction->save($this->data)) {
				$processed = $this->Transaction->process();
				if ($processed) {
					$this->Session->setFlash("Your transaction has been saved and processed ($processed)");
				}
				else {
					$this->Session->setFlash("Your transaction has been saved but failed to process ($processed)");
				}
				$this->redirect(array('action'=>'index'));
			}
		}
	}
}

Simplest possible listing of items in the database. As for the addition of the new ones, we want to validate the form submission, save data in the database, and then, if everything was OK, call a process() method, so that an appropriate processing logic is triggered. For the simplicity of the example, our transaction processing logic will just return a string – a different one for each transaction type. And we’ll display that string in the flash message.

Now, for the main course – transaction model (app/models/transaction.php):

<?php
class Transaction extends AppModel {
	const BEHAVIOR_PREFIX = 'Trnx';

	var $allowedMethods = array('CC','PP','BW');
	var $allowedCurrencies = array('USD','EUR','GBP','CHF','JPY');

	var $name = 'Transaction';
	var $validate =  array(
						'method' => array(
										'notEmpty' => array(
														'rule' => 'notEmpty',
													),
										'allowedMethod' => array(
														'rule' => array('checkAllowedMethod'),
														'message' => 'This method is not allowed',
													),
									),
						'amount' => array(
										'notEmpty' => array(
														'rule' => 'notEmpty',
													),
										'allowedAmount' => array(
														'rule' => array('checkAllowedAmount'),
														'message' => 'This amount is invalid',
													),
									),
						'currency' => array(
										'notEmpty' => array(
														'rule' => 'notEmpty',
													),
										'allowedCurrency' => array(
														'rule' => array('checkAllowedCurrency'),
														'message' => 'This currency is not allowed',
													),

									),
					);

	function beforeValidate() {
		$method = '';
		if (!empty($this->data['Transaction']['method'])) {
			$method = $this->data['Transaction']['method'];
		}
		if (in_array($method, $this->allowedMethods)) {
			$this->pushBehavior($method);
		}
		return true;
	}

	function nameBehavior($method) {
		return self::BEHAVIOR_PREFIX . strtolower($method);
	}

	function pushBehavior($method) {
		$behavior = $this->nameBehavior($method);
		$this->Behaviors->attach($behavior);
		$this->Behaviors->enable($behavior);
	}

	function popBehavior($method) {
		$behavior = $this->nameBehavior($method);
		$this->Behaviors->disable($behavior);
		$this->Behaviors->detach($behavior);
	}

	function checkAllowedMethod($check) {
		$allowed =  in_array($check['method'], $this->allowedMethods);
		return $allowed;
	}

	function checkAllowedAmount($check) {
		$result = false;

		if (method_exists($this, 'get_max_amount')) {
			$maxAmount = $this->get_max_amount();
			debug("maxAmount = $maxAmount");
			if (is_numeric($check['amount']) && $check['amount'] > 0 && $check['amount'] <= $maxAmount) {
				$result = true;
			}
		}

		return $result;
	}

	function checkAllowedCurrency($check) {
		return in_array($check['currency'], $this->allowedCurrencies);
	}
}

?>

So, what do we have here? Let’s take a closer look.

  1. Class constant BEHAVIOR_PREFIX. We’ll use it to help us with the naming convention. Nothing more.
  2. Arrays with valid options. The simplest possible data for the validation process.
  3. Validation rules. We’ll just check that specified transaction currency and transaction methods are sensible, and that transaction amount is between zero and maximum allowed amount. Here is a tricky bit: we want a different maximum amount limit for each different payment method. Stay tuned.
  4. beforeValidate() . That’s an important bit. CakePHP will call this method before the form validation kicks in. Which will kick in before the data is actually saved to the database. And that’s precisely the place where we want to identify transaction method and load an appropriate behavior. Note that this method should always return true.
  5. nameBehavior() . That’s a little helper method I use to keep construct the name of the behavior from a specified parameter, in this case – a transaction method.
  6. pushBehavior() and popBehavior() are two methods which, for the lack of better naming, attach/enable and disable/detach a specific behavior to/from the model. While pushBehavior() is called automatically from beforeValidate(), I can always use these methods manually from other places in the model or even from the controller. Handy.
  7. checkAllowedMethod() and checkAllowedCurrency() are two simple validation checks that compare a given value against a valid items list.
  8. checkAllowedAmount() – that’s the part where we check that specified amount is within appropriate limits. To get a higher limit we call getMaxAmount() method, which is common for all transaction types (stay tuned for definition). That method returns a number, which is different for each type of the transaction. And we add some extra logic to check if getmaxAmount() method actually exists, because if an invalid transaction method will be specified, then beforeValidation() will fail to load any sensible behavior and we’ll get a PHP complain here, when it will try to call an undefined method.

Now, let’s see the behaviors. Behaviors should extend the ModelBehavior class of CakePHP and they should have a setup() method that will be called when they are loaded. But since we need a number of behaviors that behave the same way, what we do instead is create a base transaction behavior, which the rest of the behaviors extend. Here is the base behavior as defined in app/models/behaviors/trnxbase.php :

<?php
require_once dirname(__FILE__) . DS . 'interfaces' . DS . 'transaction.php';

abstract class TrnxbaseBehavior extends ModelBehavior implements iTransaction {

	var $settings;
	var $max_amount = 500;

	function setup(&$Model, $settings) {
		if (!isset($this->settings[$Model->alias])) {
			$this->settings[$Model->alias] = array();
		}
		$this->settings[$Model->alias] = array_merge($this->settings[$Model->alias], (array) $settings);
	}

	function getMaxAmount(&$Model) {
		return $this->max_amount;
	}
}
?>

Important things to see here:

  1. We use an abstract class here. We won’t have a “base” transaction method and nobody should really instantiate the TrnxbaseBehavior class.
  2. We also implement an iTransaction interface, which we also require. This is a very convenient bit which helps us to make sure that all behaviors that extend the base also implement all required methods.
  3. We set a default value for our maximum amount validation. This value will be used in case one of the behaviors will not override it.
  4. setup() method is common to all transaction behaviors, so we put it here.
  5. getMaxAmount() is common to all transactions as well.

Before I forget it, here is the contents of the app/models/behaviors/interfaces/transaction.php – we only require each transaction behavior to define the process() method.

<?php

interface iTransaction {
	public function process();
}
?>

So, now all we need to do is create our behaviors – one for each transaction method. Here’s one for the credit cards (app/models/behaviors/trnxcc.php):

<?php
require_once dirname(__FILE__) . DS . 'trnxbase.php';

class TrnxccBehavior extends TrnxbaseBehavior {

	var $max_amount = 2500;

	function process() {
		return 'Charging the credit card';
	}
}
?>

Notice how we require and extend a base transaction behavior, how we set a different limit for the maximum amount limit, and how we have to define the process() method. If we forget to define the process() method, PHP will fail with a lot of noise. Try it at home.

In the meantime, here is a very similar behavior for a PayPal transaction (app/models/behaviors/trnxpp.php):

<?php
require_once dirname(__FILE__) . DS . 'trnxbase.php';

class TrnxppBehavior extends TrnxbaseBehavior {

	var $max_amount = 10000;

	function process() {
		return 'Processing PayPal transaction';
	}
}
?>

And then another one for bank wires (app/models/behaviors/trnxbw.php):

<?php
require_once dirname(__FILE__) . DS . 'trnxbase.php';

class TrnxbwBehavior extends TrnxbaseBehavior {

	var $max_amount = 100000;

	function process() {
		return 'Generating bank wire instructions PDF';
	}
}
?>

And that’s it. So just to review the above, here is what we get:

Enjoy!

Exit mobile version