ActiveRecord Model logic encapsulation

Hi there!

I’ve started my adventure with Yii today and while writing a simple blogging application (not the one from the demo pack) I’ve found something itchy :)

So here we go:

Let’s assume that we have a typical User and Post models with defined relations.




public function relations()

{

	return array(

		'posts' => array(

			self::HAS_MANY,

			'Post',

			'author_id',

			'order' => 'posts.create_time DESC',

		),

	);

}



And in the Post model




public function relations()

{

	return array(

		'author' => array(self::BELONGS_TO, 'User', 'author_id'),

	);

}



Now additionally the Post models possesses a method returning latest posts implemented as shown here:




public function findLatest($amount = 5)

{

	return $this->findAll(array(

		'order' => 'create_time DESC',

		'limit' => $amount

	));

}



If you’re implementing a single user blog system you can render the index page with a piece of code like that:




public function actionIndex()

{

	$this->render('index', array(

		'posts' => Post::model()->with('author')->findLatest(),

	));

}



Now if the blog system is designed to handle many users at the same time it would be nice to make a profile page witch would contain only posts written by a certain user determined with his ID.

Using the AR pattern and the Yii implementation we can access all of the user’s posts (within the controller) using a simple code




// Loads the User instance

$model = $this->loadModel($givenId);

$posts = $model->posts;



The problem is, that the code above returns all the posts that the user has created. Now if we need only a certain amount of sorted posts we <b>could</b> (if possible) use the Post model logic to do that.




// Loads the User instance

$model = $this->loadModel($givenId);

$posts = $model->posts->findLatest();



But the code from above <b>can’t work</b> since the posts property contains an array of already loaded posts, and not an instance of Post class.

As far as I’ve red the manual I found that there are some solutions for that - like:




$model = $this->loadModel($id);

$posts = $model->getRelated('posts', false, array(

	'limit' => 3,

	'order' => 'create_time DESC',

));



or




$model = $this->loadModel($id);

$posts = Post::model()->findAll(

	'author_id=:aid',

	array(':aid' => $model->id),

);



we could also define another relation in the User class (latestPosts) with additional conditions, that could be altered if needed. It’s almost the same as defining a method like User::getLatestPosts() internally preforming the task using a Post::model() instance.

The problem is (or I find it as a problem) that too much Post model internals leak out to other models and controllers (e.g. table names, columns, query conditions etc.) what makes it hard to maintain. Logic encapsulation would make the controller / related model code more readable and future changes in the model would propagate very fast and safely.

Maybe I’m lacking some knowledge that would let me encapsulate the logic in Yii models that I couldn’t find in the manual or the guide itself. Or, perhaps my approach is wrong.

Any workarounds for the problem? Or perhaps - an improving approach discussion :) ?

I think one solution can be to apply the named scope to the related model.

Something like this will reduce dependencies




$user = User::model()->with('posts:latest')->findByPk('id=:id', array(':id'=>$id));

$posts = $user->posts;



Of course, it will also replace $this->loadModel($id)

/Tommy

Yea, the proposed solution (although complicated) kinda fixes the problem




$model = User::model()->with('posts:latest')->findByPk($id);



Pitifully - if you want to display two sets of user’s posts (one most recent post or a set of last week posts that were created before most recent sunday) on a single page (controller’s action) you’ll have to preform two separate queries - each with different scope (e.g. ‘posts:last’ and ‘posts:lastWeek’). Now for each request you pull the user from the database together with the joined post table records filtered to fulfil the scope conditions - that’s a bit of an overhead.

But I think that’s a thing that can’t be avoided in Yii in it’s current state.

IF the model would always be an Object, and the properties representing the relations would be objects, the latter could take it’s parent’s ID and simply put it into the WHERE query condition (like parent_fk = :id) - thus preforming a relational query - and query only the table that contains the data that we need with no additional overhead.

The performance like always depends on the amount of data to deal with.

More than that - it looks like running the query like that ommits the ‘limit’ criteria defined in the Post’s scope

Scope code:




public function scopes()

{

	return array(

		'latest' => array(

			'order' => 'create_time DESC',

			'limit' => 3,

		), // latest

		'edited' => array(

			'condition' => 'create_time != update_time',

		), // edited

	);

}



Generated SQL code:




SELECT


`t`.`id` AS `t0_c0`,

`t`.`username` AS `t0_c1`,

`t`.`password` AS `t0_c2`,

`t`.`email` AS `t0_c3`,

`posts`.`id` AS `t1_c0`,

`posts`.`content` AS `t1_c1`,

`posts`.`create_time` AS `t1_c2`,

`posts`.`update_time` AS `t1_c3`,

`posts`.`author_id` AS `t1_c4`


FROM `tbl_user` `t`

LEFT OUTER JOIN `tbl_post` `posts`

ON (`posts`.`author_id`=`t`.`id`)

WHERE (`t`.`id`=1)

ORDER BY create_time DESC



Am I doing something wrong or it simply can’t be done?