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:
- 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.
- 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.
- Class constant BEHAVIOR_PREFIX. We’ll use it to help us with the naming convention. Nothing more.
- Arrays with valid options. The simplest possible data for the validation process.
- 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.
- 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.
- 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.
- 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.
- checkAllowedMethod() and checkAllowedCurrency() are two simple validation checks that compare a given value against a valid items list.
- 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:
- We use an abstract class here. We won’t have a “base” transaction method and nobody should really instantiate the TrnxbaseBehavior class.
- 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.
- 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.
- setup() method is common to all transaction behaviors, so we put it here.
- 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:
- Simplified model and controller, which only cover generic, high-level overview functionality with no details.
- A transaction specific behavior, which provides for a single place for all custom logic handling.
- A base transaction behavior, which simplifies sharing of common functionality between behaviors.
- A transaction interface super-imposed on all behaviors extending a base on. This will guarantee that our generalized model and controller will work the same no matter which behavior is loaded.
- Business logic (such as how to process each specific transaction) is decoupled from the CakePHP framework itself.
- An evolution-friendly architecture. Even if you already have a whole lot of code, splitting it up into manageable chunks and migrating to the above code organization is rather trivial and won’t require any drastic changes. For example, you can leave the interface definition completely blank for the period of migration and refactoring.
Enjoy!
Thanks for this tutorial. It took me a little while to digest all of it due to my lack of knowledge on interfaces. I had to go Googling to get myself up to speed. I’m curious, however, as to way you are using inheritance and an interface simultaneously? It seems that the base logic contained in your TrnxbaseBehavior class could just as easily be placed in your iTransaction interface. This would cut down on the number of files and classes in use and would ensure that each behavior implementing iTransaction employs the $settings, $max_amount and getMaxAmount() members. I do see, however, that if TrnxbaseBehavior were to grow fairly large, that it would contain members not necessarily required by all sub-classes. Anyway, I’m still trying to learn and think through all of this so feel free to school me!
Kevin,
yes, you are right – this being just an example, it shows how to “set up the scene” for growing things. Th base class will eventually have stuff that is not necessary in all sub classes.
This is just a way to tie an interface, base class, and sub classes together. Whether you need all parts or just some of them is up to you and up to the concrete situation. :)
Thanks for replying, good to know I’m on the right mental track :) In thinking more about this, however, I’m wondering how one would store unique data per transaction in the database? In other words, imagine you have a separate table for each transaction type to account for specific fields which pertain only to PayPal, or only to CC, etc. I see how you are dynamically attaching the correct behavior depending on the transaction type, but what about dynamically setting the Transaction::useTable property as well? I’m not sure if that would work. It also brings up issues of multiple form validation scenarios and alternative query methods depending on the transaction type in question.
Kevin,
the way we were doing it was with two tables – transactions and transaction_fields. All data common to all transaction types, such as amount, currency, etc, goes to the transactions tables. All transaction specific data goes into transaction_fields table, which is basically a key-value sort of thing (id, data_name, data_value, transaction_id, created, updated). It works quite well for some scenarios. For others though you’d be better with separate tables, separate models, etc.
Great article and great job proving the flexibility of the Cakephp framework while maintaining the conventions!
Wow! This is cool…. saved my day…cos i am doing something very similar…Thanks man