27 Apr
Fat Models, Skinny Controllers
I've become a convert to the design concept that says when you build something with an MVC pattern, you should have fat models and skinny controllers. What does this really mean? It means that you put as much of your business logic as you can into your models and all the controller should be doing is retrieving data from the model and passing it to the view. Nice and simple, but a very powerful concept once you start doing it.
So, despite my problems using the RADAR concept for the Internet Baseball League web site, I did do the models and controllers the way I wanted. Here's what I'm talking about:
(Note the use of overriding the default table and database names. CakePHP lets you use legacy databases, and don't let anyone tell you otherwise)
-
/**
-
* Model for accessing schedule table
-
*
-
* @author Chris Hartjes
-
*/
-
-
class Schedule extends AppModel
-
{
-
var $name = 'Schedule';
-
var $useTable = 'sched2007';
-
var $useDbConfig = 'stats';
-
-
function get($week) {
-
"a05" => "BOW", "a06" => "MCM", "a07" => "PHI", "a08" => "WMS",
-
"a09" => "LAW", "a10" => "PAD", "a11" => "POR", "a12" => "STL",
-
"n01" => "CSP", "n02" => "COL", "n03" => "MIN", "n04" => "SDQ",
-
"n05" => "BUZ", "n06" => "CAJ", "n07" => "DTR", "n08" => "SCS",
-
"n09" => "CRE", "n10" => "MAD", "n11" => "SEA", "n12" => "SPO");
-
$results = $this->findAll("home LIKE 'a%' AND week = " . (int)$week);
-
-
foreach ($results as $result) {
-
$home = $team[$result['Schedule']['home']];
-
$away = $team[$result['Schedule']['away']];
-
}
-
-
-
foreach ($data as $home => $matchup) {
-
$scheduledGames[$home] = $matchup;
-
}
-
-
$results = $this->findAll("home LIKE 'n%' AND week = " . (int)$week);
-
-
foreach ($results as $result) {
-
$home = $team[$result['Schedule']['home']];
-
$away = $team[$result['Schedule']['away']];
-
}
-
-
-
foreach ($data as $home => $matchup) {
-
$scheduledGames[$home] = $matchup;
-
}
-
-
return $scheduledGames;
-
}
-
}
Now *that* is a fat model. I'm manipulating all the business logic within my model and returning it. Now, here's what the controller looks like:
-
/**
-
* Controller for displaying individual weeks
-
* of the IBL schedule
-
*
-
* @author Chris Hartjes
-
*/
-
-
class ScheduleController extends AppController
-
{
-
var $name = 'Schedule';
-
var $layout = 'rosters';
-
var $cacheAction = "1 day";
-
-
function index($week)
-
{
-
$game = new Game();
-
$week = (int)$week;
-
-
if ($week == 0) {
-
$week = $game->getMaxWeek();
-
}
-
-
$schedule = new Schedule();
-
$this->set('schedule', $schedule->get($week));
-
$this->set('week', $week);
-
$this->set('maxWeek', $game->getMaxWeek());
-
$this->set('scheduleWeek', $week);
-
}
-
-
}
See, the schedule controller is nice and thin. All it does is get the week as a parameter passed into it via the URL and then tells the model to give me all me the schedule for the week. Simple, easy to understand and even easier to maintain.
I know that sometimes these things don't make sense at first but once you actually use them...and then have to go and modify something you quickly realize just what advantage you are getting from it. Try seeing if you can refactor your MVC-based app (CakePHP or otherwise) to have Fat Models and Thin Controllers.

Posted by Walker Hamilton on 27.04.07 at 5:55 pm
I just discovered this whole "fat models, skinny controllers" thing (about 1 month and a half ago) and....I love it!
I makes thing ridiculously cool and fun! You build this model that does all the right stuff to the data (on input & output) and then....no matter how you have to modify your application, saving the data is cake! ("easy"?) Retrieving it properly is pretty damn easy too.
I created system-wide variables for conditions so that the conditions are set at the right time and make it easy to exclude or include some rows when calling a model.
Posted by Daniel Hofstetter on 27.04.07 at 5:55 pm
Well, "fat models" doesn't mean the model methods should be "fat" like in your example. Fat models with skinny methods are more beautiful than fat models with fat methods
Posted by Chris Hartjes on 27.04.07 at 5:55 pm
@Daniel: Put you money where your comment is and tell me how you would refactor my model to have skinny methods. I believe I have it down to doing the minimum necessary.
Posted by Daniel Hofstetter on 27.04.07 at 5:55 pm
Sure, here a refactored model:
class Schedule extends AppModel
{
var $name = 'Schedule';
var $useTable = 'sched2007';
var $useDbConfig = 'stats';
function get($week) {
$scheduledGames = array();
$scheduledGames = $this->__getGames('a', $scheduledGames, $week);
$scheduledGames = $this->__getGames('n', $scheduledGames, $week);
return $scheduledGames;
}
function __getGames($type, $scheduledGames, $week) {
$team = array("a01" => "BUF", "a02" => "COU", "a03" => "HAG", "a04" => "TRI",
"a05" => "BOW", "a06" => "MCM", "a07" => "PHI", "a08" => "WMS",
"a09" => "LAW", "a10" => "PAD", "a11" => "POR", "a12" => "STL",
"n01" => "CSP", "n02" => "COL", "n03" => "MIN", "n04" => "SDQ",
"n05" => "BUZ", "n06" => "CAJ", "n07" => "DTR", "n08" => "SCS",
"n09" => "CRE", "n10" => "MAD", "n11" => "SEA", "n12" => "SPO");
$results = $this->findAll("home LIKE '".$type."%' AND week = " . (int)$week);
$data = array();
foreach ($results as $result) {
$home = $team[$result['Schedule']['home']];
$away = $team[$result['Schedule']['away']];
$data[$home] = array('home' => $home, 'away' => $away);
}
asort($data);
foreach ($data as $home => $matchup) {
$scheduledGames[$home] = $matchup;
}
return $scheduledGames;
}
}
Posted by Daniel Hofstetter on 27.04.07 at 5:55 pm
Hm, looks a bit ugly, but I hope you see what I mean
Posted by Walker Hamilton on 27.04.07 at 5:55 pm
Something i would do....if you need to hardcode that $team var....maybe I would move it up as a model var rather than a method var so that I could have access to it in other "skinny methods".
I tried refactoring it, but a few things made me stumble:
1. Why is there all that hard-coded data that seems like season-specific (and team-) data?
2. I didn't understand at all what the whole repeating of the data array manipulation was about (and the query). I guess I don't understand baseball seasons/scheduling at all. (Okay, so not "I guess"... "I know" that I know nothing about baseball and baseball season scheduling.)
I go to a few Sox games a year and drink beer and eat hot dogs....other than that, I know nothing about the game.
- Daniel, I totally understand your point, I just don't see how to do it in this case....as Chris says.
Posted by Ian on 27.04.07 at 5:55 pm
I'm a huge fan of the "fat models". I like using a hybrid of the 3-tier and MVC approaches.
Business Logic Layer (BLL) and Data Access Layer (DAL) do the hard work with the data.
The Web UI side is the Controller and View. Unless I have specific things that need to be done for the user side (like web controls and such), all I'm doing is retrieving data from the BLL/DAL and passing it through the view. Started out doing this in ASP.NET and then took it back to PHP and love it. Definitely makes for cleaner/maintainable code.
Posted by Pádraic Brady on 27.04.07 at 5:55 pm
It doesn't look like a fat method from here - can't see any need for it to go on a diet since it's very focused.
Fat models are cool for all sorts of reasons. One huge advantage is that business logic is now all neatly parcelled in reusable methods - in essence it's pre-empting the mistake of duplicating code across multiple controllers (a common mistake especially when the Model is viewed as little more than a data gateway).
Fat models all the way :).
Posted by developercast.com » Chris Hartjes’ Blog: Fat Models, Skinny Controllers on 27.04.07 at 5:55 pm
[...] includes an example of this school of thought - an update to a previous project that shows most of the functionality and logic (schedule information being [...]
Posted by Nate Klaiber on 27.04.07 at 5:55 pm
I recently thought of this as well when finishing up an application. I noticed that mine were almost reversed, that I have a lot of business logic in my controller. The issue arises when I need to update logic related to the model - I am now having to manipulate all of my controllers.
It's much easier (and cleaner) to keep all of that inside of the models - then your controllers are simply passing the data (and doing some logic related to auth/auth/sessions/etc).
Good example, and good example of refactored code as well. it is important to break things apart - as some of your methods can be extended/utilized in other methods as well - so keeping things organized will make your fat models well, less fat - but still powerful.
Posted by Derek Martin on 27.04.07 at 5:55 pm
I disagree.
A model should be a model, not a Logic+Model.
That being said, a controller should be a controller, not a Logic+Controller.
But how can this be?
There's a missing piece to this puzzle, and it fits between Controllers & Models.
That piece is called a Manager.
The Controller uses >=1 Manager.
The Manager contains the Logic and uses >=1 Model, and potentially other Managers. Managers also do Validation on data before putting it into Models.
The Models are just Models.
Posted by Chris Hartjes on 27.04.07 at 5:55 pm
@Derek Well, the big problem is that it's not Model-View-Manager-Controller, so one (or both) of the Model or the Controller is forced to do some managing. No way around it, really. I just think the Fat Model, Skinny Controller paradigm calls for the Logic to be in the Model.
Posted by Derek Martin on 27.04.07 at 5:55 pm
Sorry, those should have said >=0, not >=1.
Anyway, since when did an acronym (MVC) stop you from doing things in a better way? It's made my coding life easier, and my code cleaner.
Also, one of the principles of MVC is that the layers don't know about one another, so you can swap them in and out without busting the app. What happens when you want to swap out a fat model? You have to re-write a bunch of application logic, and that's no fun.
>>the Fat Model, Skinny Controller paradigm calls
>>for the Logic to be in the Model.
Innately, it does.
So maybe it's the wrong paradigm.
Posted by Chris Hartjes on 27.04.07 at 5:55 pm
@Derek Okay, I throw out the same challenge to you as I did the other commenters: refactor the code sample I gave to fit what you're talking about.
Posted by Derek Martin on 27.04.07 at 5:55 pm
Ok, this is a mutant example that looks a bit like CakePHP and a bit like ZendFramework, but I'm sure you'll get the idea. I'm hoping to turn this into a PHP|Architect article, so this is a good chance to play with these ideas in public.
Background Info:
Managers can serve as an abstraction layer above N-Models. They map more to concepts than to tables.
For instance, a UserManager might manage the User Model, Address Model, and Profile Model.
In this case I'm using ScheduleManager to manage both Schedule and Week because they appear to be highly related.
Rules of Thumb:
Managers aggregate application & validation errors from the Models it uses, and make them accessible to the Controller.
1) Keep Models as thin as possible (Handy if you regenerate your Models automatically).
2) Managers only use Models and other Managers.
3) Managers only return values -- they never echo/print.
4) Controllers only use Managers and Views. They never touch Models.
5) Controllers get data from Managers and feed it to Views, which they then render.
6) Managers house all your logic and apply it consistently (DRY), regardless of how many Controllers are using it.
7) Controllers don't contain any Logic, so changes to application flow can't accidentally break your logic.
9) Controllers are only responsible for calling Manager methods and redirecting flow based on the results (if the Manager reports errors, react accordingly).
THE EXAMPLE:
getGameWeek($week);
$view = new View();
$view->week = $week;
$view->maxWeek = $sm->getMaxWeek();
$view->scheduleWeek = $week;
$view->myGames = $sm->getWeeklyScheduleForUser($_SESSION['user_pk']);
$schedule = $sm->getScheduleForWeek($week);
if(!$schedule)
{
//Here the Manager has collected errors for us
$_SESSION['errors'] = $sm->getErrors();
//And the Controller controls application flow
header('Location: /error');
exit();
}
$view->schedule = $schedule;
echo $view->render('index');
}
}
/**
* Manager for displaying the individual weeks
* of the IBL schedule
*
* @author Derek Martin
*/
class ScheduleManager extends Manager
{
public function getMaxWeek()
{
$game = new Game();
return $game->findMax('WEEK'); //*******yes, i'm oversimplifying
}
/**
* Just a few small tweaks to your your schedule->get(week) method
*
* @param integer $week
* @return Schedule
*/
function getScheduleForWeek($week)
{
$scheduledGames = array();
$team = array("a01" => "BUF", "a02" => "COU", "a03" => "HAG", "a04" => "TRI",
"a05" => "BOW", "a06" => "MCM", "a07" => "PHI", "a08" => "WMS",
"a09" => "LAW", "a10" => "PAD", "a11" => "POR", "a12" => "STL",
"n01" => "CSP", "n02" => "COL", "n03" => "MIN", "n04" => "SDQ",
"n05" => "BUZ", "n06" => "CAJ", "n07" => "DTR", "n08" => "SCS",
"n09" => "CRE", "n10" => "MAD", "n11" => "SEA", "n12" => "SPO");
//*******THE NEXT LINE IS NEW
$schedule = new Schedule();
//*******THE NEXT LINE IS CHANGED
$results = $schedule->findAll("home LIKE 'a%' AND week = " . (int)$week);
$data = array();
foreach ($results as $result) {
$home = $team[$result['Schedule']['home']];
$away = $team[$result['Schedule']['away']];
$data[$home] = array('home' => $home, 'away' => $away);
}
asort($data);
foreach ($data as $home => $matchup) {
$scheduledGames[$home] = $matchup;
}
//*******THE NEXT LINE IS CHANGED
$results = $schedule->findAll("home LIKE 'n%' AND week = " . (int)$week);
$data = array();
foreach ($results as $result) {
$home = $team[$result['Schedule']['home']];
$away = $team[$result['Schedule']['away']];
$data[$home] = array('home' => $home, 'away' => $away);
}
asort($data);
foreach ($data as $home => $matchup) {
$scheduledGames[$home] = $matchup;
}
return $scheduledGames;
}
}
/**
* Model for accessing schedule table
*
* @author Derek Martin
*/
class Schedule extends Model
{
var $name = 'Schedule';
var $useTable = 'sched2007';
var $useDbConfig = 'stats';
}
?>
Wow, I really should post this kind of stuff on my own blog
Posted by keymaster on 27.04.07 at 5:55 pm
Chris - a question which I'm certain displays my ignorance of OO, ... but, doesn't cake instantiate all models in the controller's uses list?
If so, why do you need the:
$schedule = new Schedule();
$this->set('schedule', $schedule->get($week));
Why wouldn't:
$this->set('schedule', $this->Schedule->get($week));
have worked?
Posted by Chris Hartjes on 27.04.07 at 5:55 pm
@keymaster
You're 100% correct, and that bit of code has been refactored recently when I added in some new features to the site. Just stupidity on my part when whipping together something that Needed To Get Done(tm).
Posted by Loud Baking » Blog Archive » GridHelper - for easy grids in CakePHP on 27.04.07 at 5:55 pm
[...] but the idea is to completely separate resultset preparation from rendering re-enforcing the fat models, skinny controllers by adding skinny views [...]
Posted by Webspin on 27.04.07 at 5:55 pm
If you need to switch out a 'fat' model and are concerned about having to rewrite business logic then shouldn't that business logic be encapsulated in a behavior, and the Behavior would then function like a "Manager" class?
Posted by james on 27.04.07 at 5:55 pm
Thanks for the article ... I only wish I knew about this earlier. The CakePHP manual doesn't mention this (I don't think).
Posted by Chris Hartjes on 27.04.07 at 5:55 pm
@James
Well, it's not really a CakePHP-specific thing. It's more a application architecture issue with any framework that tries to follow the Model-View-Controller pattern.
Posted by deltawing on 27.04.07 at 5:55 pm
I think the better way to say it would be , "models should be fat in relation to controllers". That is, keep everything down to the minimum possible.
Posted by Kiran Aghor on 27.04.07 at 5:55 pm
I read somewhere about fat-model and skinny controllers. But was wondering how to code that way. This post shows it clearly. Thanks a lot.