The ControllerRole ChainAction Massacre (Part 2)
Introduction
Do you remember The ControllerRole ChainAction Massacre (Part 1)? We were innocent. Sitting on the floor, playing Lego and cuddling with some tiny little actions. It was fun!
But as Roland Deschain of Gilead, the Gunslinger, once stated: "The world has moved on!"
Now it's time for some x-rated adult stuff...
About this article
This article mostly deals with problems which might occur when reusable actions are implemented with Moose::Role. It includes some examples to explain the problems, and some hints how to solve them. It introduces Moose's BUILD and BUILDARGS methods, and shows an alternative way to add roles to Catalyst controllers, including some information about how this can influence your controllers and controller roles. It starts to look behind the scenes and somehow pushes controller roles to the next level.
The section about performance is postponed. The author himself seems to have some performance issues and could not finish it in time...
Honey, I need you!
Creating roles with attributes in them is great. Attributes make your code more configurable, flexible and reusable. When you use attributes, you don't have to care about creating getters and setters. Moose does that for you. But using attributes in roles has a drawback: You can not reliably require them. You can require methods. Attributes have accessors, which are methods. You can require attributes by requiring their accessors.
But:
Attributes, including their accessors, are created at runtime by calling has. Ok. Why not. Or: Why is this interesting? Depending on how you write your roles, the "order" in which they are applied to a class matters.
If you have a role like this:
package CatalystX::TraitFor::Controller::MyGetModel; use MooseX::MethodAttributes::Role; has stash_model_as => ( ... ); sub get_model :Chained("/") :PathPart("") :CapturArgs(0){ ... } use namespace::autoclean; 1;
and a second role like this:
package CatalystX::TraitFor::Controller::MyFoo; use MooseX::MethodAttributes::Role; requires qw/get_model stash_model_as/; sub foo :Chained("get_model") :PathPart("foo") :CapturArgs(0){ my ($self, $c) = @; my $model = $c->stash->{$self->stash_model_as}; ... } use namespace::autoclean; 1;
and you want to use both of them in your controller:
package MyApp::Controller::FooController; use Moose; extends "Catalyst::Controller"; with qw/ CatalystX::TraitFor::Controller::MyGetModel CatalystX::TraitFor::Controller::MyFoo /; no Moose; 1;
Your application will not start. Moose will complain about missing the "stash_item_as" method which is required by MyFoo, even though it would be present in the object after creation. This is not a bug. Moose complains because it has to.
Simply speaking: Roles are added by a call to with in Moose. All roles are added with the same call. The contained attributes would satisfy all requirements, but the requirements are checked before the attributes are created. MyFoo is not satisfied. Moose complains.
The Moose Manual includes a small paragraph about this, including a workaround for attributes defined in the class itself.
The "consuming-class-approach"
One way to bypass this is to make sure that the roles are added in the correct order. This can be done by manually calling "with" several times:
package MyApp::Controller::FooController; use Moose; extends "Catalyst::Controller"; with q/CatalystX::TraitFor::Controller::MyGetModel/; with q/CatalystX::TraitFor::Controller::MyFoo/; no Moose; 1;
In this scenario, "MyGetModel" is added first. Moose checks its requirements, and executes the code. All attributes and their accessors are added to the consuming class. "MyFoo" is added afterwards. It will not complain, because the required methods have already been added to the consuming class. Everything is fine.
But++:
- * Every developer who wants to use your roles has to know about their requirements, and how to fulfill them.
-
The roles can not be "just plugged" into every controller. The developer has to know some of your roles internals, and care about them. A problem which originates in your roles has to be solved in the consuming class.
- * Circular requirements can not be resolved.
-
If MyFoo requires an attribute which is included in MyBar, and MyBar requires an attribute which is included in MyFoo, both roles can not be used in the same controller without resolving the requirements manually. Yes, you are right. Circular requirements are evil and should be avoided. But it happens from time to time, and you never know how your roles will be used in the future.
The "role-itself-approach"
Another approach is not to wait for Moose to create your accessors:
package CatalystX::TraitFor::Controller::MyGetModel; use MooseX::MethodAttributes::Role; # rename the attribute has _stash_model_as => ( init_arg => "stash_model_as" ... ); # create you own setter/getter sub stash_model_as{ my $self = shift; $self->_stash_model_as(@_); } sub get_model :Chained("/") :PathPart("") :CapturArgs(0){ ... } use namespace::autoclean; 1;
Now, "stash_model_as" is not created at runtime anymore. It is an ordinary subroutine and therefore recognized by every role that requires it. The attribute itself did not change much. We can still use type constraints, coercion, delegation and all that candy. By setting the "init_arg", even the attributes key in the config hash stays the same. The role can be consumed by every controller again. The order in which roles are added to the consuming class does not matter anymore, and circular requirements are resolved.
The price we pay is obvious: We have to write more code. Additionally, accessor manipulation inherited from Class::MOP does not work anymore.
A stitch in time saves nine
At the moment, we are doing something like this in our roles:
package CatalystX::TraitFor::Controller::GetRS; ... sub get_resultset :CaptureArgs(1) :PathPart("") { ... } ...
And something like this in the consuming controller:
package MyApp::Controller::Foo; use Moose; extends "Catalyst::Controller"; with "CatalystX::TraitFor::Controller::GetRS"; ... __PACKAGE__->config( action => { get_resultset => { PathPart => "foo" }, }, ); ...
Now think of Moose as a tailor. Objects are clothes, classes (including their roles) are sewing patterns and parameters passed to the constructor are material. Catalyst controllers are suits. You can order clothes by creating dot.pm order sheets.
What we are doing above is a little bit like ordering a red suit with knobs, and adding a note to the sheet which says "please use red knobs as well". We have to remind the tailor of doing that, otherwise he would use transparent default-knobs.
This is not very clever. Let's create a smarter sewing pattern. The default pattern lets the customer choose all material (by setting __PACKAGE__->config). We should keep this behavior, but the default color should be the same as the suit itself.
Here we go: (yes, GetRS is the knob-role)
package CatalystX::TraitFor::Controller::GetRS; ... sub BUILDARGS{ my ($class, %params) = @_; # verify that no pathpart has been configured so far unless( %params && $params{action} && $params{action}->{get_resultset} && $params{action}->{get_resultset}->{PathPart} ){ # create a new pathpart, based on the class name my @split = split /::/, $class; my $pathpart = $split[-1]; $pathpart =~ tr/[A-Z]/[a-z]/; $params{action}->{get_resultset}->{PathPart} = $pathpart; } return \%params; } ... 1;
The BUILDARGS method is called as a class method at construction time. It receives all parameters passed to the constructor as is, and it is expected to return a hash reference of named parameters which is used to construct the object. The code above inserts a pathpart for the get_resultset method if none was specified before.
This example is not completely correct. If you have several roles implementing BUILDARGS, Moose will complain. If a BUILDARGS method is implemented in the consuming controller, it will silently override the role implementation. Your role will not work as expected. "Overriding" existing BUILDARGS method included in other roles will not work. Methods implemented in a role behave like methods implemented in the consuming class itself, and "override" is limited to methods which come from a superclass. This restriction is not limited to BUILDARGS. It is relevant for all role-methods.
Using the "around" modifier works fine:
around BUILDARGS => sub{ my ($orig, $class, %params) = @_; %params = %{ $class->$orig(%params)}; # This calls any other BUILDARGS implementation unless( %params && $params{action} && $params{action}->{get_resultset} && $params{action}->{get_resultset}->{PathPart} ){ my @split = split /::/, $class; my $pathpart = $split[-1]; $pathpart =~ tr/[A-Z]/[a-z]/; $params{action}->{get_resultset}->{PathPart} = $pathpart; } return \%params; };
You should always specify your BUILDARGS method like that, even in the class itself. All other implementations of BUILDARGS (up to Moose::Object, and including roles) will be called. As long as your role does not change any parameters except its own, you don't have to worry about conflicts.
All this coding in the role, just to safe a single line of code in the consuming controller...
Yes! If your role is used just 16 times, you are a winner. But that's not the point. The point is: Reasonable defaults make using your roles more intuitive. Additionally, the chance of creating bugs by misspelling or forgetting pathpart definitions decreases.
Do I have to mention that BUILDARGS is quite powerful, and not limited to pathpart-manipulation? I don't think so.
Under construction
Moose includes another way to manipulate how your objects are created: The BUILD method. It is called as object method and receives the parameter hash passed to new, after eventually updating it with BUILDARGS.
It is is often used to perform complex verification, like this:
sub BUILD{ my $self = shift; die "very complex verification failed" unless $self->do_very_complex_verification; }
But it can do more. It can even add method modifiers based on construction parameters. Let's create a role which ensures that a HTTP "person_id" parameter always matches the currently authenticated user, unless the user is a superuser:
package CatalystX::TraitFor::Controller::OverridePersonId; use MooseX::MethodAttributes::Role; sub BUILD{ my ($self, %params) = @_; my $modified_methods = $params{"override_person_id"} || ["edit"]; foreach (@$modified_methods){ if($self->can($_)){ $self->meta->add_before_method_modifier( $_, sub{ my ($self, $c) = @_; unless($c->check_user_roles(["superuser"])){ $c->req->params->{person_id} = $c->user->id; } }, ); } else{ die ref($self) . "does not implement $_. Check your configuration"; # the second dead body so far. This seems to be a peaceful massacre... } } return $self; } use namespace::autoclean; 1;
And this is how the role is used:
PACKAGE MyApp::Controller::WithPersonId; use Moose; extends 'Catalyst::Controller'; with "CatalystX::TraitFor::Controller::OverridePersonId"; sub create_something :Local { my ($self, $c) = @_; my $person_id = $c->req->param("person_id"); ... } __PACKAGE__->config( override_person_id => ["create_something"], ); no Moose; 1;
It is assumed that Catalyst::Plugin::Authentication and Catalyst::Plugin::Authorization::Roles are in use. Class::MOP's add_before_method_modifier is used to add the method modifiers. If you need access to the roles configuration within your controller, you can add an attribute for it. To make this role more reusable, the name of the overwritten parameter and the superuser-role should be configurable as well...
Specifying a BUILD method within a role results in the same problems as specifying a BUILDARGS method. Unfortunately, the BUILD method is special. It is kind of bitchy, a real drama queen. Moose relies on BUILD methods of superclasses being executed in the right order. The BUILD method of a superclass always has to be called before the extending classes BUILD method, otherwise it is not guaranteed that the base object is complete and valid. According to the Moose Manual, applying method modifiers to BUILD is not supported.
The "do-it-anyway-approach"
Nevertheless, the Moose Cookbook suggests to use after-modifiers for BUILD, like this:
sub BUILD{} after BUILD => sub{ ... };
The empty BUILD stub ensures that a BUILD method is present. If it wouldn't, it wouldn't be possible to add a method modifier for it. If the consuming class has its own BUILD, the empty stub will silently be overwritten.
This solution often works fine. But remember: It is not officially supported. I had situations where it did its job, but i also remember situations where it just didn't work out. I was not able to find out why, but after removing all method modifiers applied to BUILD, everything worked as expected. (Since I still do not know the exact reason, it is possible that it was a bug in my code...)
The "do-it-yourself-approach"
It is possible to solve this problem by yourself, within the consuming controller. Moose lets us exclude and alias role methods. If our controller does not implement its own BUILD method, and only one role includes a custom BUILD, we don't have to do anything. It will just work. If several BUILD methods are in charge, we can rename them and call them manually inside the classes BUILD:
package MyApp::Controller::WithBuild; use Moose; extends "Catalyst::Controller"; with "CatalystX::TraitFor::Controller::FirstBuildRole" => { -excludes => qw/BUILD/, -alias => { BUILD => "_first_roles_BUILD",} }, "CatalystX::TraitFor::Controller::SecondBuildRole" => { -excludes => qw/BUILD/, -alias => { BUILD => "_second_roles_BUILD",} }; sub BUILD{ my ($self, %params) = @_; $self = $self->_first_roles_BUILD(%params); $self = $self->_second_roles_BUILD(%params); # your controller's BUILD code here return $self; } no Moose; 1;
BUILD is not modified anymore. The controller's BUILD is an ordinary method and can be handled by Moose. The roles BUILD methods are renamed (in fact, they are excluded and re-added with a different name). They do not conflict with any other BUILD implementation anymore, and can therefor be called by BUILD without creating conflicts.
This approach again has the problem that the developer has to know your roles internals, and that a role-problem has to be solved by hand when the role is used. The good thing about this approach is that it will always work, as long as the developer remembers to implement it.
The first approach is much more elegant, but not officially supported. Using it can have unforeseen consequences.
No matter which way you prefer, you should always add a comment to your roles documentation if you use a BUILD method. This makes it possible to find out why things are going wrong if they are going wrong. If your documentation does not include such a hint, a developer using your role will most likely not be able to figure out the problem in case of conflicts.
Trait me well
The CPAN is huge. It contains so many modules. And most of them are really useful! CatalystX::Component::Traits for example. It is a Moose::Role for Catalyst Components, which adds support for traits. Traits are roles which are applied to an instance, and not to a class. This does matter! But we will come to that later. First, an example:
If your controller has CatalystX::Component::Traits applied to it:
package MyApp::Controller::Foo; use Moose; extends "Catalyst::Controller"; with "CatalystX::Component::Traits"; no Moose; 1;
You can enable your ControllerRoles within your applications configuration:
package MyApp; use Catalyst qw/ConfigLoader/; ... __PACKAGE__->config( "Controller::Foo" => { traits => [ "+CatalystX::TraitFor::Controller::ModelActions", "+CatalystX::TraitFor::Controller::MyFo", ], } ); ...
This is (almost) equivalent to specifying the roles in your class:
package MyApp::Controller::Foo; use Moose; extends "Catalyst::Controller"; with qw/ CatalystX::TraitFor::Controller::ModelActions CatalystX::TraitFor::Controller::Foo /; no Moose; 1;
I prefer the "classic" approach, without CatalystX::Component::Traits. It feels right to specify all the functionality and actions within the controller class, and use the config hash for configuration only. This also makes it easy to modify my controller by creating method-modifiers for methods supplied by my roles. I will edit my controller anyway. Adding a few "with"-lines does not hurt me. But this is just my personal opinion. There is more than one way to do it! I am NOT interested in a flame-war! CatalystX::Component::Traits is great. I have used it in the past, and I will use it in the future. It has useful features, like "trait searching" and "trait merging". And, of course: There are these little differences...
The fact that traits are applied to objects instead of classes matters in several ways, including the following:
It influences inheritance
If you specify your controller like this:
package MyApp::Controller::Foo; use Moose; extends "Catalyst::Controller"; with qw/ CatalystX::TraitFor::Controller::ModelActions CatalystX::TraitFor::Controller::Foo /; no Moose; 1;
and you have a second controller which extends this foo-controller:
package MyApp::Controller::Bar; use Moose; extends "MyApp::Controller::Foo"; no Moose; 1;
Bar will inherit everything from Foo, including all methods, actions and attributes specified in any of the consumed roles. This is not necessarily true when traits are in use. If you specify your traits within the applications config (as shown above), they are not part of the controller. Bar will inherit everything from Foo, but not the traits, and nothing specified in any of the traits.
It is also possible to specify the traits within the controller's config:
package MyApp::Controller::Foo; use Moose; extends "Catalyst::Controller"; with qw/ CatalystX::Component::Traits /; __PACKAGE__->config( traits => [qw/ +CatalystX::TraitFor::Controller::ModelActions +CatalystX::TraitFor::Controller::MyFoo / ], ); no Moose; 1;
In that case, Bar will inherit everything again.
It influences your code
Using traits instead of roles also influences you when you are writing reusable code with Moose::Role.
Do you remember the dynamically created "stash_model_as" attributes from The ControllerRole ChainAction Massacre (Part 1) ? The controller's class name was used to dynamically create the key which is used to store the model in the stash. The key was created like this:
sub{ my @split = split "::", ref(shift); my $controllername = pop @split; $controllername =~ tr/[A-Z]/[a-z]/; return $controllername . "_model"; }
CatalystX::Component::Traits uses MooseX::Traits::Pluggable to create a new anonymous class with all traits applied to it. This results in a class name like this:
Moose::Meta::Class::__ANON__::SERIAL::33
With our "old" code, the resulting key for our foo-controller would be
33_model
and not
foo_model
This might work if you always use the "stash_model_as" attribute, but it is ugly and risky. And think of our BUILDARGS method. It will result in the pathpart being "33" instead of "foo", which is really bad.
We can use some Moose internals to bypass this. Since we all like reusable code, lets create a subroutine which extracts the "Foo" from "MyApp::Controller::Foo", no matter if it is anonymous or not:
sub _get_base_name{ my $self = shift; my $classname; # handle anonymous class if($self->meta->is_anon_class){ # fetching superclasses my @superclasses = map { Class::MOP::class_of $_} $self->meta->linearized_isa; foreach(@superclasses){ # searching the first non-anonymous class unless($_->is_anon_class){ # store its name $classname = $_->name; last; } } } else{ # store the classname if called on an object $classname = ref($self) || $self; } # return the last part of the name pop [split /::/, $classname]; }
Or, shorter:
use List:Util qw/first/; sub _get_base_name{ my $self = shift; pop [split /::/, $self->meta->is_anon_class ? ( first { ! $_->is_anon_class } map {Class::MOP::class_of $_ } $self->meta->linearized_isa )->name : ref($self) || $self]; }
This method will return the last part of the first non-anonymous classname we are inheriting from. It will work with classes and objects.
Now we can create our default stash-key for the model like this:
sub{ shift->_get_base_name =~ tr/[A-Z]/[a-z]/r . "_model" }
Since our helper can be called as a class- and as a object-method, we can use it in our BUILDARGS method as well.
Notes:
- * Class::MOP::class_of is used to find the metaclass for a given classname
- * $metaclass->is_anon_class is used to find out which classes are anonymous
- * $metaclass->linearized_isa is used to get a list of all parent classes
- * $metaclass->name returns the classname
Thanks to the people in #moose for many useful tips.
The main difference
is, obviously, that not all objects of a class do the same roles anymore. It is possible to create two objects of the same class which have different attributes and object methods. This is not supported by CatalystX::Component::Traits, and this is most likely not what you want for your controller. All controllers of the same class are expected to handle requests equally. But you might want that in the model. Possibly if you want the model to behave different if the current request is done by an authenticated user.
Conclusion
- * Roles do not offer the same functionality as classes.
- * Most restrictions can easily be bypassed without evil hacking.
- * Carefully designing a role, and providing reasonable defaults can safe a lot of time when using it.
- * Moose offers two different ways to influence your controller's build process:
- * BUILDARGS allows you to modify parameters before the object is created.
- * BUILD allows you to modify the object after creation, but before it is returned to the calling context.
-
It is the princess in the Moose empire. The princess is beautiful, mighty but special. If you treat her right, she will be thankful and give you unforeseen power. But if she gets mad, she will throw her golden ball into the sewers and blame you, the innocent frog.
- * There is more than one way to do it!
- * If your role could be used as a trait, it might influence your code.
Author
Lukas Thiemeier <lukast@cpan.org>
Post Scriptum
Thank you for reading my article. I hope you had at least as much fun reading it as I had writing it for you.
Hints, criticism, bug fixes and everything which helps me making my articles better is very welcome! Just send a message to lukast@cpan.org.