- Published on
A DRY Piece of Cake
- Authors

- Name
- Kevin van Zonneveld
- @kvz
So I've been learning CakePHP the last few days. Bit by bit I've been trying to port a legacy administration app to Cake. 'Secretly' linking menuitems to finished Cake parts as we go. And I must say: I'm pretty excited. I did run into a disturbing conclusion though. I estimated the legacy app will have over 300 Models & Controllers once finished. That could easily add up to (300 x 4 =) 1200 views. And here I am, creating a maintenance hell while trying to solve one!
Most of my views are very similar. They're all about managing data in a table. Kind of like phpmyadmin but more from a business point-of-view. So I thought: Why not share views between different controllers.
Note that I'm** not **working on your average 2.0 site here so your mileage may vary. Extremely.
Don't duplicate views. Share them
This approach basically allows me to only create models & controllers, but no
views. Still, I have the flexibility to create custom views for the remaining
- let's say - 30 special cases. I just create a view file in the same directory as I would normally: Cake would find it, and use it instead of falling back on the default view. Simple right?
To achieve that, these were my steps.
N.B. The code that I provide here are stripped versions. I don't want to bother you with my specifics.
1. Enrich Models
Added information to the models explaining what fields are important, if they
have special rules. They will be interpreted by the AppController and then
fed to the default views in the form of a $showFields array.
2. AppController Fallback
This is the most important. We need to tell the AppController to switch to
the default view if a custom view cannot be found:
<?php
class AppController extends Controller {
/**
* Set up all required view data just before viewing
*/
function beforeRender() {
/*
* Switch to default view if specific controller view cannot be
* translates: app/views/dev_issues/index.ctp [NOT FOUND!]
* to: app/views/default/index.ctp
*
* Enabling you to only create views if you have very deviating ones, and have all
* standard objects handled by a dynamic default view.
*/
$viewPath = reset(Configure::read('viewPaths'));
$subDir = isset($this->subDir) && !is_null($this->subDir) ? $this->subDir . DS : null;
$name = $this->action;
$name = str_replace('/', DS, $name);
$name = $this->viewPath . DS . $subDir . Inflector::underscore($name);
$path = $viewPath . $name . $this->ext;
if (!file_exists($path)) {
// Change the viewPath so it will now try to load $this->action
// from the 'default' directory
$this->viewPath = 'default';
}
// Next I retrieve some Model->schema() information, enrich it,
// and then I provide it to the view like so: $this->set('showFields', $showFields);
// then the the 'default' views can iterate it and do some magic
}
?>
(parts of this code were taken from the Cake core, and may need to be optimised for this situation)
3. Create Default Views
Of course we can't skip this step. I created an '/app/views/default'
directory, and some common actions. Just as if default were a normal
controller, and so we have the following layout:
/views
/default
index.ctp
view.ctp
add.ctp
edit.ctp
I added code to those views so they can dynamically iterate through any
important Model fields they encounter in $showFields which is set by the
AppController. For instance, my edit.ctp contains the following code:
<?php
// Iterate through all the fields that are interesting in UI
// the $showFields is basically a modified Model->schema(), set by the
?>
AppController
<?php
foreach ($showFields as $fieldName=>$fieldData) {
echo $form->input($fieldName, $fieldData['formOptions']);
}
// All important fields will be automagically drawn
?>
4. Resolve Helper
To avoid adding too much logic to views, I store some logic in helpers.
For example I created the ResolveHelper, it's called from views, and will
translate any field value to a more meaningful one. This is done according to
rules in the Model like it's relation to parent Models.
Fat AppController
Because my Models are so similar there will be 300 Controllers, each having at
least their own index, view, edit & add method:
<?php
Class DevIssuesController extends AppController {
function index() {
}
function view($id = null) {
}
function edit($id = null) {
}
function add() {
}
}
?>
Now that's a lot of duplication right there. 300 index() methods? 300
add() methods? I don't think so.
So I moved out the most basic Controller methods (index, view, and the
like) to the app_controller.php file, which in fact, all other controllers
are based on.
This actually behaves in the same way as shared views. Basically:
AppController->index() will be called for every Controller unless you
specifically set one like DevIssuesController->index(). This is just OOP
nature that we take advantage of.
So basically I only add some common methods to my AppController and be done
with it. For the 30 Controllers that are too exceptional to be served by these
common methods, I can still write custom methods inside them. I can even call
parent::index(); to still make use of the AppController logic, and expand
the method with additional logic as well.
Of course these common methods will need some extra routines so they can handle the common tasks decoupled from specific models. Keys in this approach are:
- Allow for some configuration in your Models
- Stick to conventions
In my case, I ended up with a common edit method something like this:
<?php
function edit($id = null) {
if (empty($this->MyModel)) {
$this->Session->setFlash(__('Generic AppController->edit cannot be' .
'used for this Controller. Please create a method: '.
$this->name.'->'.__FUNCTION__.'()', true));
}
if (!$id && empty($this->data)) {
$this->Session->setFlash(__('Invalid ' . $this->MyModel->title . '', true));
return $this->redirect(array('action'=>'index'));
}
if (!empty($this->data)) {
if ($this->MyModel->save($this->data)) {
$this->Session->setFlash(__('The '.$this->MyModel->title.' has been saved', true));
$this->redirect(array('action'=>'index'));
} else {
$this->Session->setFlash(__('The '.$this->MyModel->title.'could not be saved. Please, try again.', true));
}
}
// Set Main data
if (empty($this->data)) {
$this->data = $this->MyModel->read(null, $id);
}
$data = array();
$this->set($data);
}
?>
I may need to expand it a bit in the future to enable support for depending Models, etc. But that's all pretty straight forward. And the point is: I will only need to make these changes in one place. And all other 300 objects could profit from it.
What About Baking?
Baking is really good if you stick to Cake's conventions.
If you're working with legacy data like me, you may define things like
$useTable, $primaryKey, and $foreignKey. But you may find that these
properties are pretty much ignored by the baking process.
Resulting in text fields where you would expect select boxes, and id: '21032' where you would expect: 'Kevin'.
Besides, I would still end up with 1200+ views that I cannot change from that point forward, because all would be lost on the next bake. Also, the views may respond differently depending on environment variables like logged in user.
What About Scaffolding?
Well scaffolding is not at all recommended for production use, and I feel my current approach gives me more flexibility. Because by adding a bit of configuration to models I really have fine-grained control over how the views behave for different models. Reducing the need for exceptions. Reducing the need for duplication.
Downsides
Some people may feel that I'm coupling MVC more than I should: a Model knows about Controller aspects (though not much, I try to use the controller to enforce its logic on the Model as much as possible).
While I am aware of these dangers, I feel this approach will also allow me to create the 'legacy app' 50 times faster and so it really solves more problems for me than it creates.
Besides, this is only true for the default system. Nothing stops me from
supplying Cake with additional real views, Controllers with real methods, and
they will be fully adhering to MVC.
Conclusion
So while I don't usually like my Cake DRY, I'm very happy with the way this is going!
Now, though I may have some programming skills, I'm new to Cake, so I really welcome insightful comments on all this.
Legacy Comments (12)
These comments were imported from the previous blog system (Disqus).
Hey, interesting post. One suggestion I\'d make is that, if you don\'t actually have any unique code in any of your controllers, you could just have one meta-controller class, and pipe all your requests through it using a custom route.
You could then use the routing information to make the meta-controller load the correct model class, etc.
@ nate: Awesome. That way I guess I would only have to setup models and be done with it :) This is going to be very neat.
Thanks man, I\'ll definitely give it a shot.
PS. it actually was your talk @ kings of code in Amsterdam last year that really sparked my Cake enthusiasm, so once again: Thanks :)
When you start your project, May i know the reason you choose cakephp or mvc ?
You can further reduce the duplication if your add/edit forms are identical (mine usually are) by using $this->render() in you add/edit methods:
[CODE=\"php\"]
function add() {
// add login
$this->render(\"form\");
}
function edit($id = null) {
// edit logic
$this->render(\"form\");
}
[/CODE]
You then only need to create one form.ctp in your view directory.
Bah, missed the closing quote in the CODE mark up >.<
You really need a preview option for numpties like me!
@ joy: Well working on this legacy app has really made me aware of the dangers of not:
- avoiding duplication
- separating logic from layout
- etc
So I\'ve been looking for more elegance for a long time. I\'ve looked into & created some frameworks & libraries myself, but then I found Cake.
Or better yet: Felix Geisendörfer (http://debuggable.com) pointed me in that direction, and then what really pulled me in was the presentation of Nate Abele. He probably does a better job of explaining than I can do:
http://www.slideshare.net/g...
@ Richard@Home: I took the liberty of adding that quote again ;)
Great suggestion, it worked beautifully. I just renamed my edit.ctp to form.ctp and all is set :)
@ Nate: Ok so I got it working. I did have some issues with the Router, but that will probably boil down to my lack of experience with Cake. Here\'s what I did.
First of all, I liked to maintain my original url structure. So no:
/defaults/controller/action
but just:
/controller/action
routes.php now contains routes for my DefaultsController, this is kind of a catch-all:
[CODE=\"PHP\"]
/*
* Routing to default Controller
* to avoid having to create a duplicated controller for every model
*/
Router::connect(
\'/:usecontroller/:action/:id/*\',
array(
\'controller\' => \'defaults\'
),
array(
\'pass\' => array(\'id\'),
\'usecontroller\' => \'[0-9a-z_]+\',
\'action\' => $Action,
\'id\' => $ID
)
);
Router::connect(
\'/:usecontroller/:action/*\',
array(
\'controller\' => \'defaults\'
),
array(
\'usecontroller\' => \'[0-9a-z_]+\',
\'action\' => $Action,
)
);
Router::connect(
\'/:usecontroller/*\',
array(
\'controller\' => \'defaults\',
\'action\' => \'index\'
),
array(
\'usecontroller\' => \'[0-9a-z_]+\'
)
);
[/CODE]
And it also contains custom routes for controllers that simply cannot be served by the DefaultsController. The exceptions.
[CODE=\"PHP\"]
/*
* Some models still require their own controllers
*/
Router::connect(
\'/employees/*\',
array(
\'controller\' => \'employees\'
)
);
[/CODE]
Maybe I could dynamically generate them by iterating over the controllers that are actually defined in /app/controllers/* ?
OK, and then I have the DefaultsController in /app/controllers/defaults_controller.php
[CODE=\"PHP\"]
<?php
/**
* This default controller will jump in if no specific
* controller has been defined in the Router
*/
class DefaultsController extends AppController {
/**
* We need the actual controller name fast so CakePHP
* can setup model relations, etc.
*/
function __construct() {
// Determine actual controller name
// Router hasn\'t been loaded yet.
// So use dirty URL hack to determine actual Name
$parts = explode(\'/\', $_SERVER[\'REQUEST_URI\']);
$nothing = array_shift($parts);
$base = array_shift($parts);
$useController = array_shift($parts);
// Overrule controller name
$this->name = Inflector::camelize($useController);
parent::__construct();
}
/**
* We need to teach Router the URLs to this actual Controller name
* So that $Html->link() doesn\'t return /default/* for all default controllers
*/
function beforeFilter() {
$_this =& Router::getInstance();
// setRequestInfo is a dragg, you need to know all the current vars
// or the Router will be corrupted.
$_this->__params[0][\'controller\'] = Inflector::underscore($this->name);
parent::beforeFilter();
}
}
?>
[/CODE]
Well, got it working... I won\'t say beautifully ;)
But I would like to learn how I could have done this better. So if you feel like insulting my code.. Be my guest ;)
I can almost say that I had a deja vù while looking to your code. I did exactly like this when I first step in cake\'s world a year or so ago. It\'s really dry and fast, and probably a totally logical solution - it\'s like buil apps on the fly.
The scenario I had was exactly the one you\'ve got: a large application only aiming maintenance and administration of resources - a fairly big CRM. But one thing happened while the baby was growing, and began to walk other than just crawl, I realized that:
- there\'s no reason for a CRUD path for every Model on the system;
- central controllers, with CRUD functionality to be passed for subclasses should build a callback hell while trying to get other things done within data creation or edition - like emailing the client owner;
- it occasionally makes a mess when using Components to deal with some output or template changing;
and a few more things...
Anyway, I think it\'s a nice thing to do and to get to know various aspects of cake\'s insides, but you endup realizing that it\'s not what your application needs, and you could tackle more than half of the actions and views if you had a heavy and consistent UI design an development.
@ Rafael Baindeira: Thanks for your comment. Though I am a cake rookie and you\'re probably right, let me stick to your arguments...
> - there\'s no reason for a CRUD path for every Model on the system;
Well there doesn\'t have to be. I can easily have e.g.
[CODE=\"Javascript\"]
Class MyModel extends AppModel {
var $noDefaultRendering = true;
}
[/CODE]
that\'s for people accidentally landing on those pages. otherwise: I won\'t even create links to models that don\'t need an interface.
> - central controllers, with CRUD functionality to be passed for
> subclasses should build a callback hell while trying to get other things
> done within data creation or edition like emailing the client owner;
I don\'t know if I\'m getting you right but Can\'t I just:
[CODE=\"Javascript\"]
Class MyController extends AppContoller {
function add() {
// mail client here
// back to normal action
parent::add();
}
}
[/CODE]
> - it occasionally makes a mess when using Components to deal with
> some output or template changing; and a few more things...
Well for now it saves me creating about a 1000 php files, so if there are certain models that won\'t work well with certain layouts: I shall simply create a controller for them & add a custom layout.. Right?
half-right. Actually, there\'s no right or wrong, it\'s all about opnions.
Just one more coin, about calling the parent method, sure it\'s possible, but what if a data creation implies something more during the process? You will endup rewriting the code adjusting where needed, and as the system grows and the client demands modifications, these kind of changes will finish kill the implementation....
anyway, as I said, my experience on the matter. It\'s a good approach - i did it - but just saying that a consistent and hard-worked UI plan would solve it right away.
@ Rafael Bandeira: OK thanks for your input Rafael. Please allow me to be stubborn for now, and have the pleasure of saying: \'I told you so\' once I come crying back to you ;)