RailsConf Europe 2008 - Ian White wrote resources_controller in the beginning of 2007 to DRY up RESTful controllers. In its first iteration it simplified controllers by providing CRUD actions, and loading enclosing resources.
4. What I’ll cover
Example of RESTful app in vanilla Rails
How was that experience? Where did we have to
repeat ourselves?
ardes.com
5. What I’ll cover
Example of RESTful app in vanilla Rails
How was that experience? Where did we have to
repeat ourselves?
Plugins to the rescue!
ardes.com
6. What I’ll cover
Example of RESTful app in vanilla Rails
How was that experience? Where did we have to
repeat ourselves?
Plugins to the rescue!
resources_controller
ardes.com
7. What I’ll cover
Example of RESTful app in vanilla Rails
How was that experience? Where did we have to
repeat ourselves?
Plugins to the rescue!
resources_controller
Benefits of abstraction
ardes.com
8. What I’ll cover
Example of RESTful app in vanilla Rails
How was that experience? Where did we have to
repeat ourselves?
Plugins to the rescue!
resources_controller
Benefits of abstraction
Where to next?
ardes.com
14. Video sharing app
Users can contribute Videos
Videos can be categorised in many Categories
ardes.com
15. Video sharing app
class User < ActiveRecord::Base
has_manycan contribute Videos
Users :videos
end
Videos can be categorised in many Categories
class Categorisation < ActiveRecord::Base
belongs_to :video
belongs_to :category
end
class Category < ActiveRecord::Base
has_many :categorisations
has_many :videos, :through => :categorisations
end
class Video
belongs_to :user
has_many :categorisations
has_many :categories, :through => :categorisations
ardes.com
end
19. Video sharing app
Our API?
Lets define the URLs
we want to see all videos: /videos
ardes.com
20. Video sharing app
Our API?
Lets define the URLs
we want to see all videos: /videos
we want to see all categories: /categories
ardes.com
21. Video sharing app
Our API?
Lets define the URLs
we want to see all videos: /videos
we want to see all categories: /categories
we want to look at videos in the context of one of their
categories: /categories/:category_id/videos/:id
ardes.com
22. Video sharing app
Our API?
Lets define the URLs
we want to see all videos: /videos
we want to see all categories: /categories
we want to look at videos in the context of one of their
categories: /categories/:category_id/videos/:id
WAIT! does this violate some community norm about
nesting?
ardes.com
23. Video sharing app
Our API?
Lets define the URLs
we want to see all videos: /videos
we want to see all categories: /categories
we want to look at videos in the context of one of their
categories: /categories/:category_id/videos/:id
WAIT! does this violate some community norm about
nesting?
No - in this case, it’s necessary, because videos can
be categorised in many categories, and because we
just want to show the video in the context of a category
that the user has selected.
ardes.com
24. Video sharing app
Our API?
Lets define the URLs
we want to see all videos: /videos
we want to see all categories: /categories
we want to look at videos in the context of one of their
categories: /categories/:category_id/videos/:id
WAIT! does this violate some community norm about
map.resources :videos
nesting?
No - in this case, it’s necessary, because videos can
map.resources in:categories and because we
be categorised many categories, do |category|
category.resources in the context of a category
just want to show the video :videos
that the user has selected.
end
ardes.com
26. Video sharing app
Because this is a community site, lets not have admin
namespaced controllers just yet
ardes.com
27. Video sharing app
Because this is a community site, lets not have admin
namespaced controllers just yet
We want users to be able submit videos in the context
of a category, and have that categorisation
automatically applied
ardes.com
28. Video sharing app
Because this is a community site, lets not have admin
namespaced controllers just yet
We want users to be able submit videos in the context
of a category, and have that categorisation
automatically applied
What controllers do we need?
ardes.com
29. Video sharing app
Because this is a community site, lets not have admin
namespaced controllers just yet
class We want users to be able submit videos in the context
VideosController < ApplicationController
of a category, and have that categorisation
# GET /videos, GET /videos.xml
automatically applied
def index
What= Video.find(:all)
@videos
controllers do we need?
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @videos }
end
end
# And the rest of CRUD ...
end
ardes.com
30. Video sharing app
Because this is a community site, lets not have admin
namespaced controllers just yet
class We want users to be able submit videos in the context
VideosController < ApplicationController
of a category, and have that categorisation
# GET /videos, GET /videos.xml
automatically applied
def index
What= Video.find(:all)
controllers do we need?
class CategoriesController < ApplicationControl
@videos
respond_to /categories, GET /categories.xml
# GET do |format|
def index
format.html # index.html.erb
@categories = Category.find(:all)
format.xml { render :xml => @videos }
end respond_to do |format|
end format.html # index.html.erb
format.xml { render :xml => @categories }
end
# And the rest of CRUD ...
end end
ardes.com
# And the rest of CRUD ...
31. class CategoryVideosController < ApplicationC
Video sharing app
before_filter :load_category
# GET /categories/:category_id/videos
Because this is a community site, lets not have admin
# GET /categories/:category_id/videos.xml
namespaced controllers just yet
def index
We want users to be able submit videos in the context
class VideosController < ApplicationController
of a category, and=have that categorisation
@videos @category.videos.find(:all)
# GET /videos, GET /videos.xml
automatically applied do |format|
respond_to
def index format.html # index.html.erb
What= Video.find(:all)
controllers do we need?
class CategoriesController < ApplicationControl
@videos
# GET /categories,{ GET /categories.xml
format.xml
respond_to do |format|
render :xml => @videos }
end
def index
format.html # index.html.erb
end
@categories = Category.find(:all)
format.xml { render :xml => @videos }
end respond_to do |format|
# And the rest of CRUD ...
end format.html # index.html.erb
format.xml { render :xml => @categories }
protected
end
# And the def load_category
rest of CRUD ...
end end @category = Category.find(params[:categor
ardes.com
end
# And the rest of CRUD ...
32. map.resources :videos
class CategoryVideosController < ApplicationC
map.resourcesbefore_filter |category|
:categories do :load_category
Video sharing app
category.resources :videos, :controller => 'category_videos'
end
# GET /categories/:category_id/videos
Because this is a community site, lets not have admin
# GET /categories/:category_id/videos.xml
namespaced controllers just yet
def index
We want users to be able submit videos in the context
class VideosController < ApplicationController
of a category, and=have that categorisation
@videos @category.videos.find(:all)
# GET /videos, GET /videos.xml
automatically applied do |format|
respond_to
def index format.html # index.html.erb
What= Video.find(:all)
controllers do we need?
class CategoriesController < ApplicationControl
@videos
# GET /categories,{ GET /categories.xml
format.xml
respond_to do |format|
render :xml => @videos }
end
def index
format.html # index.html.erb
end
@categories = Category.find(:all)
format.xml { render :xml => @videos }
end respond_to do |format|
# And the rest of CRUD ...
end format.html # index.html.erb
format.xml { render :xml => @categories }
protected
end
# And the def load_category
rest of CRUD ...
end end @category = Category.find(params[:categor
ardes.com
end
# And the rest of CRUD ...
33. map.resources :videos
class CategoryVideosController < ApplicationC
map.resourcesbefore_filter |category|
:categories do :load_category
Video sharing app
category.resources :videos, :controller => 'category_videos'
end
# GET /categories/:category_id/videos
Because this is a community site, lets not have admin
# GET /categories/:category_id/videos.xml
namespaced controllers just yet
def index
We want users to be able submit videos in the context
class VideosController < ApplicationController
of a category, and=have that categorisation
@videos @category.videos.find(:all)
# GET /videos, GET /videos.xml
automatically applied do |format|
respond_to
def index format.html # index.html.erb
What= Video.find(:all)
controllers do we need?
class CategoriesController < ApplicationControl
@videos
# GET /categories,{ GET /categories.xml
format.xml
respond_to do |format|
render :xml => @videos }
end
def index
format.html # index.html.erb
class Video < ActiveRecord::Base
attr_accessor end
@categories = Category.find(:all)
:category_for_new_record
# and a create hook to create the:xml => @videos }
format.xml { render categorisation
end end respond_to do |format|
# And the rest of CRUD ...
end format.html # index.html.erb
class Category < format.xml { render :xml => @categories }
ActiveRecord::Base
protected
has_many :videos, :through => :categorisations do
end
#def new(*args) load_category
And the defrest of CRUD ...
end @category = Category.find(params[:categor
end # pass proxy_owner through as :category_for_new_record
end ardes.com
end end
end # And the rest of CRUD ...
37. Video sharing app
What views do we need?
CRUD stuff
What about videos vs category_videos?
ardes.com
38. Video sharing app
What views do we need?
CRUD stuff
What about videos vs category_videos?
Can use partials to factor out common stuff - but
there’s a cruical difference throughout
ardes.com
39. Video sharing app
What views do we need?
CRUD stuff
What about videos vs category_videos?
Can use partials to factor out common stuff - but
there’s a cruical difference throughout
In videos we want links and forms to new_video,
video
ardes.com
40. Video sharing app
What views do we need?
CRUD stuff
What about videos vs category_videos?
Can use partials to factor out common stuff - but
there’s a cruical difference throughout
In videos we want links and forms to new_video,
video
In category_videos we want links to
new_category_video, and category_video.
ardes.com
41. Video sharing app
What views do we need?
CRUD stuff
What about videos vs category_videos?
Can use partials to factor out common stuff - but
there’s a cruical difference throughout
In videos we want links and forms to new_video,
video
In category_videos we want links to
new_category_video, and category_video.
We could use polymorphic_url, but we still need to
give it different arguments. (just the same as for the
controllers)
ardes.com
42. Video sharing app
views/categories/index.html.erb
What views do we need?
views/categories/show.html.erb
CRUD stuff
views/categories/edit.html.erb
views/categories/new.html.erb
What about videos vs category_videos?
Can use partials to factor out common stuff - but
there’s a cruical difference throughout
In videos we want links and forms to new_video,
video
In category_videos we want links to
new_category_video, and category_video.
We could use polymorphic_url, but we still need to
give it different arguments. (just the same as for the
controllers)
ardes.com
43. Video sharing app
views/categories/index.html.erb
What views do we need?
views/categories/show.html.erb
CRUD stuff
views/categories/edit.html.erb
views/categories/new.html.erb
What about videos vs category_videos?
Can use partials to factor out common stuff - but
views/videos/index.html.erbthroughout
there’s a cruical difference
views/videos/show.html.erb forms to new_video,
In videos we want links and
views/videos/edit.html.erb
video
views/videos/new.html.erb want links to
In category_videos we
new_category_video, and category_video.
We could use polymorphic_url, but we still need to
give it different arguments. (just the same as for the
controllers)
ardes.com
44. Video sharing app
views/categories/index.html.erb
What views do we need?
views/categories/show.html.erb
CRUD stuff
views/categories/edit.html.erb
views/categories/new.html.erb
What about videos vs category_videos?
Can use partials to factor out common stuff - but
views/videos/index.html.erbthroughout
there’s a cruical difference
views/videos/show.html.erb forms to new_video,
In videos we want links and
views/videos/edit.html.erb
video
views/videos/new.html.erb want links to
In category_videos we
new_category_video, and category_video.
views/category_videos/index.html.erb still need to
We could use polymorphic_url, but we
views/category_videos/show.html.erb
give it different arguments. (just the same as for the
controllers)
views/category_videos/edit.html.erb
views/category_videos/new.html.erb
ardes.com
47. Video sharing app
With vanilla Rails, it’s not too bad
4 models, 3 controllers, 12 views, 3 lines of
routes
ardes.com
48. Video sharing app
With vanilla Rails, it’s not too bad
4 models, 3 controllers, 12 views, 3 lines of
routes
But, it doesn’t feel very DRY, does it?
ardes.com
49. Video sharing app
With vanilla Rails, it’s not too bad
4 models, 3 controllers, 12 views, 3 lines of
routes
But, it doesn’t feel very DRY, does it?
Let’s see what happens if we want to add some
features
ardes.com
51. Video sharing app: new feature
We want to be able browse videos in the context
of the user that submitted them (similar to the
way we can browse by category)
ardes.com
52. Video sharing app: new feature
We want to be able browse videos in the context
of the user that submitted them (similar to the
way we can browse by category)
(Lets assume we’ve added a UsersController
and views for browsing users themselves)
ardes.com
53. map.resources :videos
map.resources :categories do |category|
category.resources :videos, :controller => 'category_videos'
end Video sharing app: new feature
map.resources :users do |user|
user.resources :videos, :controller => 'user_videos'
end We want to be able browse videos in the context
of the user that submitted them (similar to the
way we can browse by category)
(Lets assume we’ve added a UsersController
and views for browsing users themselves)
ardes.com
54. map.resources :videos
map.resources :categories do |category|
category.resources :videos, :controller => 'category_videos'
end class UserVideosControllerapp: new feature
Video sharing < ApplicationController
before_filter :load_user
map.resources :users do |user|
user.resources :videos, :controller => 'user_videos'
# GET /users/:user_id/videos
end We want /users/:user_id/videos.xml the context
# GET to be able browse videos in
of the user that submitted them (similar to the
def index
way we can browse by category)
@videos = @user.videos.find(:all)
(Letsrespond_to do |format| UsersController
assume we’ve added a
and views for browsing users themselves)
format.html # index.html.erb
format.xml { render :xml => @videos }
end
end
# And the rest of CRUD ...
protected
def load_user
@user = User.find(params[:user_id])
end ardes.com
end
55. map.resources :videos
map.resources :categories do |category|
category.resources :videos, :controller => 'category_videos'
end class UserVideosControllerapp: new feature
Video sharing < ApplicationController
before_filter :load_user
map.resources :users do |user|
user.resources :videos, :controller => 'user_videos'
# GET /users/:user_id/videos
end We want /users/:user_id/videos.xml the context
# GET to be able browse videos in
of the user that submitted them (similar to the
def index
way we can browse by category)
@videos = @user.videos.find(:all)
(Letsrespond_to do |format| UsersController
assume we’ve added a
and views for browsing users themselves)
format.html # index.html.erb
format.xml { render :xml => @videos }
end
end
# And the rest of CRUD ...
views/user_videos/index.html.erb
protected
views/user_videos/show.html.erb
def load_user
views/user_videos/edit.html.erb
@user = User.find(params[:user_id])
views/user_videos/new.html.erb
end ardes.com
end
57. Video sharing app: new feature
Another controller, and 4 more views, not bad
ardes.com
58. Video sharing app: new feature
Another controller, and 4 more views, not bad
But our videos controllers all look very similar
ardes.com
59. Video sharing app: new feature
Another controller, and 4 more views, not bad
But our videos controllers all look very similar
How about a superclass abstraction?
ardes.com
60. Video sharing app: new feature
Another controller, and 4 more views, not bad
But our videos controllers all look very similar
How about a superclass abstraction?
Problem: the action methods don’t look very easy
to abstract
ardes.com
61. # VideosController (simplified)
def destroy
@video = Video.find(params[:id])
Video sharing app:
@video.destroy new feature
redirect_to videos_path
end
Another controller, and 4 more views, not bad
But our videos controllers all look very similar
How about a superclass abstraction?
Problem: the action methods don’t look very easy
to abstract
ardes.com
62. # VideosController (simplified)
def destroy
@video = Video.find(params[:id])
Video sharing app:
@video.destroy new feature
redirect_to videos_path
end
Another controller, and 4 more views, not bad
# CategoryVideosController
def destroy videos controllers all look very similar
But our
@video = @category.videos.find(params[:id])
How about a superclass abstraction?
@video.destroy action methods don’t look very easy
Problem: the
redirect_to category_videos_path(@category)
to abstract
end
ardes.com
63. # VideosController (simplified)
def destroy
@video = Video.find(params[:id])
Video sharing app:
@video.destroy new feature
redirect_to videos_path
end
Another controller, and 4 more views, not bad
# CategoryVideosController
def destroy videos controllers all look very similar
But our
@video = @category.videos.find(params[:id])
How about a superclass abstraction?
@video.destroy action methods don’t look very easy
Problem: the
redirect_to category_videos_path(@category)
to abstract
end
# UserVideosController
def destroy
@video = @user.videos.find(params[:id])
@video.destroy
redirect_to user_videos_path(@user)
end
ardes.com
65. Video sharing app: new feature
Users should be able to comment on videos,
users, and categories
ardes.com
66. Video sharing app: new feature
Users should be able to comment on videos,
users, and categories
This looks like a job for :polymorphic
ardes.com
67. Video sharing app: new feature
class User < ActiveRecord::Base
has_many :comments, :as => :commentable
Users should be able to comment on
end
videos,
users, and categories
Video < ActiveRecord::Base
class This looks like a job for :polymorphic
has_many :comments, :as => :commentable
end
class Category < ActiveRecord::Base
has_many :comments, :as => :commentable
end
class Comment
belongs_to :commentable, :polymorphic => true
end
ardes.com
69. Video sharing app: new feature
Now, one controller - or three controllers
ardes.com
70. Video sharing app: new feature
Now, one controller - or three controllers
If we have one controller we face these problems
ardes.com
71. Video sharing app: new feature
Now, one controller - or three controllers
If we have one controller we face these problems
- how to load up the comment?
ardes.com
72. Video sharing app: new feature
Now, one controller - or three controllers
If we have one controller we face these problems
- how to load up the comment?
- how to redirect to the commentable?
ardes.com
73. Video sharing app: new feature
Now, one controller - or three controllers
If we have one controller we face these problems
- how to load up the comment?
- how to redirect to the commentable?
So lets try three controllers
ardes.com
74. map.resources :videos do |video|
video.resources :comments, :controller => 'video_comments'
end
map.resources :categories do sharing app: new feature
Video |category|
category.resources :videos, :controller => 'category_videos'
category.resources :comments, :controller => 'category_comments'
end
Now, one controller - or three controllers
map.resources :users do |user|
If we have one controller we face these problems
user.resources :videos, :controller => 'user_videos'
- how to load up :controller =>
user.resources :comments, the comment? 'user_comments'
end
- how to redirect to the commentable?
So lets try three controllers
ardes.com
75. map.resources :videos do |video|
video.resources :comments, :controller => 'video_comments'
end
map.resources :categories do sharing app: new feature
Video |category|
category.resources :videos, :controller => 'category_videos'
category.resources :comments, :controller => 'category_comments'
end
class UserCommentsController < ApplicationController
Now, one controller - or three controllers
before_filter :load_user
map.resources :users do |user|
def index If we have one controller we face these problems
user.resources :videos, :controller => 'user_videos'
@comments = @user.comments.find(:all)
- how to load up :controller =>
user.resources :comments, the comment? 'user_comments'
respond_to do |format|
format.html # index.html.erb
end
- how to redirect to the commentable?
format.xml { render :xml => @comments }
end
end
So lets try three controllers
# And the rest of CRUD ...
protected
def load_user
@user = User.find(params[:user_id])
end
end
ardes.com
76. map.resources :videos do |video|
video.resources :comments, :controller => 'video_comments'
end
map.resources :categories do sharing app: new feature
Video |category|
category.resources :videos, :controller => 'category_videos'
category.resources :comments, :controller => 'category_comments'
end
class UserCommentsController < ApplicationController
Now, one controller - or three controllers
before_filter :load_user
map.resources :users do |user| ApplicationController
def index If we have one controller we face these problems
class CategoryCommentsController <
user.resources :videos, :controller => 'user_videos'
before_filter :load_category
@comments = @user.comments.find(:all)
- how to load up :controller =>
user.resources :comments, the comment? 'user_comments'
respond_to do |format|
def index
format.html # index.html.erb
end
- how |format|
end respond_to doto redirect to the commentable?
@comments = @category.comments.find(:all)
format.xml { render :xml => @comments }
end
format.html{try three controllers}
So lets #render :xml => @comments
format.xml
index.html.erb
# And end rest of CRUD ...
the
end
protected
def # And the rest of CRUD ...
load_user
@user = User.find(params[:user_id])
protected
end
end def load_category
@category = Category.find(params[:category_id])
end
end
ardes.com
77. map.resources :videos do |video|
video.resources :comments, :controller => 'video_comments'
end
map.resources :categories do sharing app: new feature
Video |category|
category.resources :videos, :controller => 'category_videos'
category.resources :comments, :controller => 'category_comments'
end
class UserCommentsController < ApplicationController
Now, one controller - or three controllers
before_filter :load_user
map.resources :users do |user| ApplicationController
def index If we have one controller we face these problems
class CategoryCommentsController <
user.resources :videos, :controller => 'user_videos'
before_filter :load_category
@comments = @user.comments.find(:all)
- VideoCommentsController < ApplicationController
user.resources :comments, the comment? 'user_comments'
class how to load up :controller =>
respond_to do |format|
def index
format.html # index.html.erb
end before_filter :load_video
- how |format|
end respond_to doto redirect to the commentable?
@comments = @category.comments.find(:all)
format.xml { render :xml => @comments }
def index
format.html # index.html.erb
end
format.xml {try@video.comments.find(:all)
So lets =render :xmlcontrollers}
@comments three => @comments
the respond_to do |format|
# And end rest of CRUD ...
end format.html # index.html.erb
protected format.xml { render :xml => @comments }
end
def # And the rest of CRUD ...
load_user
end
@user = User.find(params[:user_id])
protected
end
end # And the rest of CRUD ...
def load_category
@category = Category.find(params[:category_id])
endprotected
end def load_video
@video = Video.find(params[:video_id])
end
end ardes.com
78. map.resources :videos do |video|
video.resources :comments, :controller => 'video_comments'
end
map.resources :categories do sharing app: new feature
Video |category| views/user_comments/index.html.erb
category.resources :videos, :controller => 'category_videos'
views/user_comments/show.html.erb
category.resources :comments, :controller => 'category_comments'
views/user_comments/edit.html.erb
end
class UserCommentsController < ApplicationController
views/user_comments/new.html.erb
Now, one controller - or three controllers
before_filter :load_user
map.resources :users do |user| ApplicationController
def index If we have one controller we face these problems
class CategoryCommentsController <
user.resources :videos, :controller => 'user_videos'
before_filter :load_category
@comments = @user.comments.find(:all)
- VideoCommentsController < ApplicationController
user.resources :comments, the comment? 'user_comments'
class how to load up :controller =>
respond_to do |format|
def index
format.html # index.html.erb
end before_filter :load_video
- how |format|
end respond_to doto redirect to the commentable?
@comments = @category.comments.find(:all)
format.xml { render :xml => @comments }
def index
format.html # index.html.erb
end
format.xml {try@video.comments.find(:all)
So lets =render :xmlcontrollers}
@comments three => @comments
the respond_to do |format|
# And end rest of CRUD ...
end format.html # index.html.erb
protected format.xml { render :xml => @comments }
end
def # And the rest of CRUD ...
load_user
end
@user = User.find(params[:user_id])
protected
end
end # And the rest of CRUD ...
def load_category
@category = Category.find(params[:category_id])
endprotected
end def load_video
@video = Video.find(params[:video_id])
end
end ardes.com
79. map.resources :videos do |video|
video.resources :comments, :controller => 'video_comments'
end
map.resources :categories do sharing app: new feature
Video |category| views/user_comments/index.html.erb
category.resources :videos, :controller => 'category_videos'
views/user_comments/show.html.erb
category.resources :comments, :controller => 'category_comments'
views/user_comments/edit.html.erb
end
class UserCommentsController < ApplicationController
views/user_comments/new.html.erb
Now, one controller - or three controllers
before_filter :load_user
map.resources :users do |user| views/category_comments/index.html.erb
def index If we have one controller we face these problems
class CategoryCommentsController < ApplicationController
user.resources :videos, :controller => 'user_videos'
before_filter :load_category views/category_comments/show.html.erb
@comments = @user.comments.find(:all)
- VideoCommentsController < ApplicationController
user.resources :comments, the comment? 'user_comments'
class how to load up :controller =>
respond_to do |format|
def index
format.html # index.html.erb
views/category_comments/edit.html.erb
end before_filter :load_video views/category_comments/new.html.erb
- how |format|
end respond_to doto redirect to the commentable?
@comments = @category.comments.find(:all)
format.xml { render :xml => @comments }
def index
format.html # index.html.erb
end
format.xml {try@video.comments.find(:all)
So lets =render :xmlcontrollers}
@comments three => @comments
the respond_to do |format|
# And end rest of CRUD ...
end format.html # index.html.erb
protected format.xml { render :xml => @comments }
end
def # And the rest of CRUD ...
load_user
end
@user = User.find(params[:user_id])
protected
end
end # And the rest of CRUD ...
def load_category
@category = Category.find(params[:category_id])
endprotected
end def load_video
@video = Video.find(params[:video_id])
end
end ardes.com
80. map.resources :videos do |video|
video.resources :comments, :controller => 'video_comments'
end
map.resources :categories do sharing app: new feature
Video |category| views/user_comments/index.html.erb
category.resources :videos, :controller => 'category_videos'
views/user_comments/show.html.erb
category.resources :comments, :controller => 'category_comments'
views/user_comments/edit.html.erb
end
class UserCommentsController < ApplicationController
views/user_comments/new.html.erb
Now, one controller - or three controllers
before_filter :load_user
map.resources :users do |user| views/category_comments/index.html.erb
def index If we have one controller we face these problems
class CategoryCommentsController < ApplicationController
user.resources :videos, :controller => 'user_videos'
before_filter :load_category views/category_comments/show.html.erb
@comments = @user.comments.find(:all)
- VideoCommentsController < ApplicationController
user.resources :comments, the comment? 'user_comments'
class how to load up :controller =>
respond_to do |format|
def index
format.html # index.html.erb
views/category_comments/edit.html.erb
end before_filter :load_video views/category_comments/new.html.erb
- how |format|
end respond_to doto redirect to the commentable?
@comments = @category.comments.find(:all)
format.xml { render :xml => @comments }
def index
format.html # index.html.erb
end
format.xml {try@video.comments.find(:all)
So lets =render :xmlcontrollers}
@comments three => @comments
views/video_comments/index.html.erb
the respond_to do |format|
# And end rest of CRUD ...
views/video_comments/show.html.erb
end format.html # index.html.erbviews/video_comments/edit.html.erb
protected format.xml { render :xml => views/video_comments/new.html.erb
@comments }
end
def # And the rest of CRUD ...
load_user
end
@user = User.find(params[:user_id])
protected
end
end # And the rest of CRUD ...
def load_category
@category = Category.find(params[:category_id])
endprotected
end def load_video
@video = Video.find(params[:video_id])
end
end ardes.com
82. Video sharing app: new feature
Damn, we forgot about commenting on videos
within the user and category contexts. (Let’s just
pretend that the customer, naively thinks that this
is something easy and obvious to do).
ardes.com
83. Video sharing app: new feature
Damn, we forgot about commenting on videos
within the user and category contexts. (Let’s just
pretend that the customer, naively thinks that this
is something easy and obvious to do).
Faced with the prospect of creating a bunch more
controllers and views, people start becoming
religious about Never nesting more than two
resources deep
ardes.com
84. Video sharing app: new feature
Damn, we forgot about commenting on videos
within the user and category contexts. (Let’s just
pretend that the customer, naively thinks that this
is something easy and obvious to do).
Faced with the prospect of creating a bunch more
controllers and views, people start becoming
religious about Never nesting more than two
resources deep
But, the reasons we might want to do that haven’t
gone away, so we need to resort to some other
tricks, or just legislate against them
ardes.com
86. Video sharing app: rc’d
For the curious, this is what our app (without
comments - will come back to that later) would
look like when using resources_controller
ardes.com
87. class UsersController < ApplicationController
Video sharing app: rc’d
resources_controller_for :users
end
For the curious, this is what our app (without
comments - will come back to that later) would
look like when using resources_controller
class VideosController < ApplicationController
resources_controller_for :videos
end
class CategoriesController < ApplicationController
resources_controller_for :categories
end
ardes.com
88. Where is the logic?
class VideosController < ApplicationController
resources_controller_for :videos
end
ardes.com
89. Where is the logic?
class VideosController < ApplicationController
resources_controller_for :videos
end
# the controller resource logic is in routes.rb!
map.resources :videos
map.resources :categories do |category|
category.resources :videos
end
map.resources :users do |user|
user.resources :videos
end
ardes.com
91. back to Rails w/o rc
Seems like we have to do a lot of almost repeating
ourselves
ardes.com
92. back to Rails w/o rc
Seems like we have to do a lot of almost repeating
ourselves
- in actions
ardes.com
93. back to Rails w/o rc
Seems like we have to do a lot of almost repeating
ourselves
- in actions
- redirecting
ardes.com
94. back to Rails w/o rc
Seems like we have to do a lot of almost repeating
ourselves
- in actions
- redirecting
- when loading enclosing resources
ardes.com
95. back to Rails w/o rc
Seems like we have to do a lot of almost repeating
ourselves
- in actions
- redirecting
- when loading enclosing resources
- in views
ardes.com
96. back to Rails w/o rc
Seems like we have to do a lot of almost repeating
ourselves
- in actions
- redirecting
- when loading enclosing resources
- in views
It seems like the reverse of the way it should be (start
with fewer classes and views, branch when behaviour
requires it)
ardes.com
99. Plugins to the rescue!
resources_controller
others
- make_resourceful
- resource_this
- resource_controller
- and more...
ardes.com
100. Plugins to the rescue!
resources_controller
others
- make_resourceful
- resource_this
- resource_controller
- and more...
They all come with a set of actions that cover CRUD,
ardes.com
101. Plugins to the rescue!
resources_controller
others
- make_resourceful
- resource_this
- resource_controller
- and more...
They all come with a set of actions that cover CRUD,
they all wrap up the pattern of loading enclosing
resources in some fashion
ardes.com
106. resources_controller: Goals
NOT scaffolding - but it can be used for that
stop repeating all of those actions
loading enclosing resources
make referring to related resource URLs easy
ardes.com
107. resources_controller: Goals
NOT scaffolding - but it can be used for that
stop repeating all of those actions
loading enclosing resources
make referring to related resource URLs easy
principle of least surprise
ardes.com
108. resources_controller: Goals
NOT scaffolding - but it can be used for that
stop repeating all of those actions
loading enclosing resources
make referring to related resource URLs easy
principle of least surprise
Not a black box - RC internals should be able
to be used for solving other problems
ardes.com
111. Scaffolding vs abstraction
Both are appropriate solutions for different problems
In my view, many of the patterns that we’ve seen in
RESTful controllers warrant abstraction
ardes.com
112. Scaffolding vs abstraction
Both are appropriate solutions for different problems
In my view, many of the patterns that we’ve seen in
RESTful controllers warrant abstraction
They enable some nice features in rc
ardes.com
113. Scaffolding vs abstraction
Both are appropriate solutions for different problems
In my view, many of the patterns that we’ve seen in
RESTful controllers warrant abstraction
They enable some nice features in rc
They enable some nice solutions to REST
oriented problems
ardes.com
114. Scaffolding vs abstraction
Both are appropriate solutions for different problems
In my view, many of the patterns that we’ve seen in
RESTful controllers warrant abstraction
They enable some nice features in rc
They enable some nice solutions to REST
oriented problems
Analogy: has_many and belongs_to, are not
implemented as sql scaffolds, but they could be.
Why is abstraction more appropriate here?
ardes.com
115. Scaffolding vs abstraction
Both are appropriate solutions for different problems
In my view, many of the patterns that we’ve seen in
RESTful controllers warrant abstraction
They enable some nice features in rc
They enable some nice solutions to REST
oriented problems
Analogy: has_many and belongs_to, are not
implemented as sql scaffolds, but they could be.
Why is abstraction more appropriate here?
The problem is clearly defined
ardes.com
116. Scaffolding vs abstraction
Both are appropriate solutions for different problems
In my view, many of the patterns that we’ve seen in
RESTful controllers warrant abstraction
They enable some nice features in rc
They enable some nice solutions to REST
oriented problems
Analogy: has_many and belongs_to, are not
implemented as sql scaffolds, but they could be.
Why is abstraction more appropriate here?
The problem is clearly defined
Abstraction enables cool stuff (like association
proxies)
ardes.com
119. resources_controller: abstraction
What’s the problem?
The model name is tightly coupled
with controller actions and views
ardes.com
120. resources_controller: abstraction
What’s the problem?
The model name is tightly coupled
with controller actions and views
so are the named routes
ardes.com
121. resources_controller: abstraction
What’s the problem?
The model name is tightly coupled
with controller actions and views
so are the named routes
and so is the resource service
ardes.com
122. class ForumsController < class UsersController <
ApplicationController ApplicationController
# GET /forums # GET /users
def index def index
@forums = Forum.find(:all) @users = User.find(:all)
end end
# GET /forums/1 # GET /users/1
def show def show
@forum = Forum.find(params[:id]) @user = User.find(params[:id])
end end
# DELETE /forums/1 # DELETE /users/1
def destroy def destroy
@forum = Forum.find(params[:id]) @user = User.find(params[:id])
@forum.destroy @user.destroy
redirect_to forums_path redirect_to users_path
end end
123. class ForumsController < class UsersController <
ApplicationController ApplicationController
# GET /forums # GET /users
def index def index
@forums = Forum.find(:all) @users = User.find(:all)
end end
# GET /forums/1 # GET /users/1
def show def show
@forum = Forum.find(params[:id]) @user = User.find(params[:id])
end end
# DELETE /forums/1 # DELETE /users/1
def destroy def destroy
@forum = Forum.find(params[:id]) @user = User.find(params[:id])
@forum.destroy @user.destroy
redirect_to forums_path redirect_to users_path
end end
124. class ForumsController < class UsersController <
ApplicationController ApplicationController
# GET /forums # GET /users
def index def index
@forums = Forum.find(:all) @users = User.find(:all)
end end
# GET /forums/1 # GET /users/1
def show def show
@forum = Forum.find(params[:id]) @user = User.find(params[:id])
end end
# DELETE /forums/1 # DELETE /users/1
def destroy def destroy
@forum = Forum.find(params[:id]) @user = User.find(params[:id])
@forum.destroy @user.destroy
redirect_to forums_path redirect_to users_path
end end
125. class ForumsController < class UsersController <
ApplicationController ApplicationController
# GET /forums # GET /users
def index def index
@forums = Forum.find(:all) @users = User.find(:all)
end end
# GET /forums/1 # GET /users/1
def show def show
@forum = Forum.find(params[:id]) @user = User.find(params[:id])
end end
# DELETE /forums/1 # DELETE /users/1
def destroy def destroy
@forum = Forum.find(params[:id]) @user = User.find(params[:id])
@forum.destroy @user.destroy
redirect_to forums_path redirect_to users_path
end end
127. de-couple the name: resource
def destroy
self.resource = find_resource
resource.destroy
#
end
ardes.com
128. de-couple the name: resource
def destroy
self.resource = find_resource
resource.destroy
#
end
this will, e.g. set @forum to Forums.find(params[:id])
ardes.com
129. de-couple the name: resource
def destroy
self.resource = find_resource
resource.destroy
#
end
this will, e.g. set @forum to Forums.find(params[:id])
we can refer to whatever the current resource is with resource
ardes.com
131. de-couple named routes:
resource(s)_path
def destroy
self.resource = find_resource
resource.destroy
redirect_to resources_path
end
ardes.com
132. de-couple named routes:
resource(s)_path
def destroy
self.resource = find_resource
resource.destroy
redirect_to resources_path
end
all named routes are supported, e.g.
formatted_resource_tags_url(‘js’)
ardes.com
133. de-couple named routes:
resource(s)_path
def destroy
self.resource = find_resource
resource.destroy
redirect_to resources_path
end
all named routes are supported, e.g.
formatted_resource_tags_url(‘js’)
think of this as named routes
relativised to the current resource
ardes.com
135. de-couple class or association:
resource_service
def find_resource(id = params[:id])
resource_service.find id
end
ardes.com
136. de-couple class or association:
resource_service
def find_resource(id = params[:id])
resource_service.find id
end
resource_service is an ActiveRecord class,
or an association proxy
ardes.com
137. de-couple class or association:
resource_service
def find_resource(id = params[:id])
resource_service.find id
end
resource_service is an ActiveRecord class,
or an association proxy
(it’s actually a bit more than that - but think of it that way)
ardes.com
138. resource_service
def find_resources
resource_service.find :all
end
def find_resource(id = params[:id])
resource_service.find id
end
def new_resource(attributes = params[resource_name])
resource_service.new attributes
end
ardes.com
139. resource_service
def find_resources
resource_service.find :all, :order => params[:order]
end
def find_resource(id = params[:id])
resource_service.find id
end
def new_resource(attributes = params[resource_name])
resource_service.new attributes
end
ardes.com
140. load enclosing resources
it’s easy, but repetitive.
you need to do it, for url/model integrity
ardes.com
141. load enclosing resources
it’s easy, but repetitive.
you need to do it, for url/model integrity
the information is already expressed
in routes.rb (most of the time)
ardes.com
142. load enclosing resources
it’s easy, but repetitive.
you need to do it, for url/model integrity
the information is already expressed
in routes.rb (most of the time)
resources_controller thinks that one controller
for multiple routes is a fine idea
ardes.com
153. resource specification
routing does the work of extracting
logical reosurces from the url
and then THROWS IT AWAY
in the mean time: RC re-recognizes the route,
and parses it according to the specification
ardes.com
155. principle of least surprise
class UserForumsController < ApplicationController
resources_controller_for :forums, :in => :user
def mark_as_boring
@forum = @user.forums.find(params[:id])
@forum.boringed_by!(@user)
redirect_to user_forums_path(@user)
end
end
ardes.com
156. principle of least surprise
class UserForumsController < ApplicationController
resources_controller_for :forums, :in => :user
def mark_as_boring
@forum = @user.forums.find(params[:id])
@forum.boringed_by!(@user)
redirect_to user_forums_path(@user)
end
end
<%= @forum.title %>
<%= @user.name %>
ardes.com
157. principle of least surprise
class UserForumsController < ApplicationController
resources_controller_for :forums, :in => :user
def mark_as_boring
@forum = @user.forums.find(params[:id])
@forum.boringed_by!(@user)
redirect_to user_forums_path(@user)
end
end
<%= @forum.title %>
<%= @user.name %>
<%= link_to @forum.name, polymorphic_url [@user, @forum] %>
ardes.com
158. Back to our video app
What if we want to add comments on videos, users,
categories - and also have them in user_videos and
category_videos?
ardes.com
159. class UsersController < ApplicationController
resources_controller_for :users
end
Back to our video app
class VideosController < ApplicationController
What if we want to add comments on videos, users,
resources_controller_for :videos
end categories - and also have them in user_videos and
category_videos?
class CategoriesController < ApplicationController
resources_controller_for :categories
end
class CommentsController < ApplicationController
resources_controller_for :comments
nested_in :commentable, :polymorphic => true
end
ardes.com
160. map.resources :videos, :has_many => :comments
map.resources :categories ApplicationController
class UsersController < do |category|
category.resources :videos,:users
resources_controller_for :has_many => :comments
end
end
map.resources our video<app
Back to :users do |user|
class VideosController ApplicationController
user.resources want to add :has_many on videos, users,
What if we :videos, comments => :comments
resources_controller_for :videos
endend categories - and also have them in user_videos and
category_videos?
class CategoriesController < ApplicationController
resources_controller_for :categories
end
class CommentsController < ApplicationController
resources_controller_for :comments
nested_in :commentable, :polymorphic => true
end
ardes.com
161. map.resources :videos, :has_many => :comments
map.resources :categories ApplicationController
class UsersController < do |category|
category.resources :videos,:users
resources_controller_for :has_many => :comments
end
end
map.resources our video<app
Back to :users do |user|
class VideosController ApplicationController
user.resources want to add :has_many on videos, users,
What if we :videos, comments => :comments
resources_controller_for :videos
endend categories - and also have them in user_videos and
category_videos?
With RC
class CategoriesController < ApplicationController
resources_controller_for :categories
You start with fewer controllers and views
end
You make more when you have too much behaviour crammed into them
class CommentsController < ApplicationController
resources_controller_for :comments
nested_in :commentable, :polymorphic => true
end
ardes.com
167. account pattern
map.resource :account do |account|
Account pattern account.resources :images
account.resources :posts
end
ardes.com
168. account pattern
map.resource :account do |account|
Account pattern account.resources :images
account.resources :posts
end
class AccountController < ApplicationController
resources_controller_for :account, :class => User, :singleton => true do
current_user
end
end
ardes.com
169. account pattern
map.resource :account do |account|
Account pattern account.resources :images
account.resources :posts
end
class AccountController < ApplicationController
resources_controller_for :account, :class => User, :singleton => true do
current_user
end
end
# You don't need to make an AccountImages, or AccountPosts controller!
class ApplicationController < ActionController::Base
map_enclosing_resource :account, :class => User, :singleton => true do
current_user
end
# or you can put the above in each controller which has :account in its
# enclosing resources, or in a mixin
end ardes.com
172. ordering a collection
map.resources :users do |user|
users.resources :things, :collection => {:order => :put}
end
class Thing < ActiveRecord::Base
acts_as_list
def self.order_by_ids(ids)
transaction do
ids.each_index do |i|
Thing.update_attribute! :position => i+1
end
end
end
end ardes.com
173. ordering a collection
map.resources :users do |user|
users.resources :things, :collection => {:order => :put}
end
def order
resource_service.order_by_ids[quot;#{resource_name}_orderquot;]
end
class Thing < ActiveRecord::Base
acts_as_list
def self.order_by_ids(ids)
transaction do
ids.each_index do |i|
Thing.update_attribute! :position => i+1
end
end
end
end ardes.com
174. Join model controllers
class ModeratorshipsController < Applica...
resources_controller_for :moderatorships,
:actions => nil
def create # ...
def destroy # ...
end
175. class ModeratorshipsController < Applica...
resources_controller_for :moderatorships,
:actions => nil
def create
self.resource = new_resource
if resource.save
flash[:notice] = “#{resource_name} created”
else
flash[:error] = ‘oops’
end
redirect_to enclosing_resource_path
end
def destroy # ...
176. class ModeratorshipsController < Applica...
resources_controller_for :moderatorships,
:actions => nil
def create # ...
end
def destroy
self.resource = find_resource
resource.destroy
flash[:notice] = “#{resource_name} destroyed”
redirect_to enclosing_resource_path
end
177. test controller plugins
garlic do
repo 'rails', :url => 'git://github.com/rails/rails'
repo 'rspec', :url => 'git://github.com/ianwhite/rspec'
repo 'rspec-rails', :url => 'git://github.com/ianwhite/rspec-rails'
repo 'resources_controller', :path => '.'
target 'edge', :branch => 'master'
target '2.0-stable', :branch => 'origin/2-0-stable'
target '2.1-stable', :branch => 'origin/2-1-stable'
target '2.1.0', :tag => 'v2.1.0'
all_targets do
prepare do
plugin 'resources_controller', :clone => true
plugin 'rspec'
plugin('rspec-rails') { sh quot;script/generate rspec -fquot; }
end
run do
cd ‘vendor/plugins/resources_controller’ do
sh quot;rake spec:rcov:verifyquot;
end ardes.com
end
end
181. RC summary
makes RESTful controllers and views easier
- Start with fewer controllers/views - make more when
required
ardes.com
182. RC summary
makes RESTful controllers and views easier
- Start with fewer controllers/views - make more when
required
sightings
ardes.com
183. RC summary
makes RESTful controllers and views easier
- Start with fewer controllers/views - make more when
required
sightings
- rc just got forked by revolutionhealth on github
ardes.com
184. RC summary
makes RESTful controllers and views easier
- Start with fewer controllers/views - make more when
required
sightings
- rc just got forked by revolutionhealth on github
- Lots of production apps
ardes.com
185. RC summary
makes RESTful controllers and views easier
- Start with fewer controllers/views - make more when
required
sightings
- rc just got forked by revolutionhealth on github
- Lots of production apps
well speced
ardes.com
187. What next?
abstract idea of resource, passed by routing to
controller on the request (you were invoked with this
resource)
ardes.com
188. What next?
abstract idea of resource, passed by routing to
controller on the request (you were invoked with this
resource)
refactoring?
ardes.com
189. What next?
abstract idea of resource, passed by routing to
controller on the request (you were invoked with this
resource)
refactoring?
core-list talk about loading resources in controllers
ardes.com