One of my first tasks in CakePHP was to produce a paginated list of blog posts and each post's tags on the same page. A straightforward couple of SQL queries would've done the job nicely, I kept reminding myself over the many hours wasted stumbling through poorly-structured documentation as I failed milestone deadlines. I was doing something very wrong.
Wu Wei is very important when you start off with a new framework. Your preconceptions will slow you down, especially if you're very experienced with the framework's language, or you've enjoyed another framework in the same language. When you find yourself thinking something as popular as CakePHP was made wrong or its behavior fails some expectation, the real problem is probably a prejudice on your part. Those carefree days of CodeIgniter, that Golden Age of frameworkless ignorance, are leading you to think Cake should act according to your expectations. Abandon that preconception and suddenly you're free to swim with the flow of your framework instead of against it. Yes, in another metaphor I'd be asking you to drink the Kool-Aid, but not all Kool-Aid is poisoned/Strawberry flavor.
The answer to my proximate problem wasn't really in the manual. I didn't find it in any other blogs, either. I found it by ignoring my expectations and paying attention to what I was writing in context of Cake, not PHP.
Here's what I added to my PostsController::index:
$this->Post->PostsTag->bindModel(array(
'belongsTo' => array('Post'),
));
$this->set('posts', $this->Post->PostsTag->find('all', array(
'recursive' => 2,
'group' => 'PostsTag.post_id',
)));
The relationship between PostsTag and Post doesn't occur automatically, it must be stated explicitly. A little recursion then brings in all the missing post tags. If a Post has more than one tag, you'll see it duplicated accordingly in your results set; easily solved with a little GROUPing. I'm doing straight find()s for these examples, but Cake makes it trivial to add pagination.
Let's say you want to do the same thing—produce a list of Posts with Tags for each post—but you only want to display those Posts which have a certain tag. You can do pretty much the same thing, adding Tag to your bindings:
$this->Post->PostsTag->bindModel(array(
'belongsTo' => array('Post', 'Tag'),
));
$this->set('posts', $this->Post->PostsTag->find('all', array(
'conditions' => array('Tag.name' => 'CakePHP'),
'recursive' => 2,
'group' => 'PostsTag.post_id',
)));
You'll note I've kept the GROUP in there. It's unlikely you'll allow users to give posts the same tag twice, but you may want to specify an OR condition, for example, in a simple tag-or-title search:
$this->Post->PostsTag->bindModel(array(
'belongsTo' => array('Post', 'Tag'),
));
$this->set('posts', $this->Post->PostsTag->find('all', array(
'conditions' => array(
'OR' => array(
'Tag.name' => 'CakePHP',
'Post.title LIKE' => "%CakePHP%",
),
),
'recursive' => 2,
'group' => 'PostsTag.post_id',
)));
One final note: if you'd like to avoid the constipation of binding directly from the controller, set up an expects() method within the PostsTag model, which you'll have to create.
By the time I realized all this, I'd settled for a clunky, straight-SQL solution that's already causing all sorts of unnecessary condition-checking in my view. While I've been swimming against the flow of the framework, my poop has been floating downstream and piling up under the sensitive noses of industrious, dam-building beavers. If I'd stopped thinking in terms of my existing skillset and instead become the uncarved block, I could be enjoying a delicious slice of Cake instead of fishing turds out of my code. To paraphrase the ancient Taoist philosopher, Cypher:
You know, I know this cake doesn't exist. I know that when I put it in my mouth, the Matrix is telling my brain it is icing-coated and delicious. Also, its documentation is very well-structured and explains things quite thoroughly without any vague or contradictory statements. And after nine years, you know what I realize? Ignorance is bliss.
PS: CakePHP is made of candy.