1 package CatalystX::DynamicComponent;
2 use MooseX::Role::Parameterized;
3 use MooseX::Types::Moose qw/Str CodeRef HashRef ArrayRef/;
5 use Moose::Util::TypeConstraints;
6 use List::MoreUtils qw/uniq/;
7 use namespace::autoclean;
9 enum __PACKAGE__ . '::ResolveStrategy' => qw/
14 our $VERSION = 0.000001;
21 parameter 'pre_immutable_hook' => (
23 predicate => 'has_pre_immutable_hook',
29 default => sub { {} },
30 resolve_strategy => 'merge',
34 default => sub { [] },
35 resolve_strategy => 'merge',
39 default => sub { [] },
40 resolve_strategy => 'replace',
44 # Shameless metaprogramming.
45 foreach my $name (keys %parameters) {
46 my $resolve_strategy = delete $parameters{$name}->{resolve_strategy};
48 parameter $name, %{ $parameters{$name} };
50 parameter $name . '_resolve_strategy' => (
51 isa => __PACKAGE__ . '::ResolveStrategy',
52 default => $resolve_strategy,
56 # Code refs to implement the strategy types
57 my %strategies = ( # Right hand precedence where appropriate
58 replace => sub { $_[1] ? $_[1] : $_[0]; },
60 if (ref($_[0]) eq 'ARRAY') {
61 [ uniq( @{ $_[0] }, @{ $_[1] } ) ];
64 Catalyst::Utils::merge_hashes(shift, shift);
69 # Wrap all the crazy up in a method to generically merge configs.
70 my $get_resolved_config = sub {
71 my ($name, $p, $config) = @_;
72 my $get_strategy_method_name = $name . '_resolve_strategy';
73 my $strategy = $strategies{$p->$get_strategy_method_name()};
74 $strategy->($p->$name, $config->{$name})
75 || $parameters{$name}->{default}->();
81 my $pre_immutable_hook = $p->pre_immutable_hook;
84 my ($app, $name, $config) = @_;
85 my $appclass = blessed($app) || $app;
92 my $component_name = $appclass . '::' . $name;
93 my $meta = Moose->init_meta( for_class => $component_name );
95 my @superclasses = @{ $get_resolved_config->('superclasses', $p, $config) };
96 push(@superclasses, 'Catalyst::' . $type) unless @superclasses;
97 $meta->superclasses(@superclasses);
99 my $methods = $get_resolved_config->('methods', $p, $config);
100 foreach my $method_name (keys %$methods) {
101 $meta->add_method($method_name => $methods->{$method_name});
104 if (my @roles = @{ $get_resolved_config->('roles', $p, $config) }) {
105 Moose::Util::apply_all_roles( $component_name, @roles);
108 if ($p->has_pre_immutable_hook) {
109 if (!ref($pre_immutable_hook)) {
110 $app->$pre_immutable_hook($meta, $config);
113 $pre_immutable_hook->($meta, $config);
117 $meta->make_immutable;
119 my $instance = $app->setup_component($component_name);
120 $app->components->{ $component_name } = $instance;
130 CatalystX::DynamicComponent - Parameterised Moose role providing functionality to build Catalyst components at runtime.
134 package My::DynamicComponentType;
136 use namespace::autoclean;
138 with 'CatalystX::DynamicComponent' => {
139 name => '_setup_one_of_my_components', # Name of injected method
142 after setup_components => sub { shift->_setup_all_my_components(@_); };
144 sub _setup_all_my_components {
146 my $app = ref($self) || $self;
147 foreach my $component_name ('Controller::Foo') {
148 my %component_config = %{ $c->config->{$component_name} };
149 # Shallow copy so we avoid stuffing methods back in the config, as that's lame!
150 $component_config{methods} = {
151 some_method => sub { 'foo' },
154 # Calling this method creates a component, and registers it in your application
155 # This component will subclass 'MyApp::ControllerBase', do 'MyApp::ControllerRole'
156 # and have a method called 'some_method' which will return the value 'foo'..
157 $self->_setup_one_of_my_components($app . '::' . $component_name, \%component_config);
163 use namespace::autoclean;
165 +My::DynameComponentType
169 'Controller::Foo' => {
170 superclasses => [qw/MyApp::ControllerBase/],
171 roles => [qw/MyApp::ControllerRole/],
178 CatalystX::DynamicComponent aims to provide a flexible and reuseable method of building L<Roles|Moose::Role>
179 which can be added to L<Catalyst> applications, which generate components dynamically at application
180 startup using the L<Moose> meta model.
182 Thi is implemented as a parametrised role which curries a
183 component builder method into your current package at application time.
185 Authors of specific dynamic component builders are expected implement an application class
186 roles which composes this role, and their own advice after the C<< setup_compontents >>
187 method, which will call the component generation method provided by using this role once
188 for each component you wish to create.
194 B<Required> - The name of the component generator method to curry.
198 Optional, a hash reference with keys being method names, and values being a L<Class::MOP::Method>,
199 or a plain code ref of a method to apply to
200 the dynamically generated package before making it immutable.
204 Optional, an array reference of roles to apply to the generated component
208 Optional, an array reference of superclasses to give the generated component.
210 If this is not defined, and not passed in as an argument to the generation method,
211 then Catalyst::(Model|View|Controller) will used as the base class (as appropriate given
212 the requested namespace of the generated class, otherwise Catalyst::Component will be used.
214 FIXME - Need tests for this.
216 =head2 pre_immutable_hook
218 Optional, either a coderef, which will be called with the component $meta and the merged $config,
219 or a string name of a method to call on the application class, with the same parameters.
221 This hook is called after a component has been generated and methods added, but before it is made
222 immutable, constructed, and added to your component registry.
224 =head1 CURRIED COMPONENT GENERATOR
232 $component_name (E.g. C<< MyApp::Controller::Foo >>)
236 $config (E.g. C<< $c->config->{$component_name} >>)
242 It is possible to set each of the roles, methods and superclasses parameters for each generated package
243 individually by defining those keys in the C< $config > parameter to your curried component generation method.
245 By default, roles and methods supplied from the curried role, and those passed as config will be merged.
247 Superclasses, no the other hand, will replace those from the curried configuration if passed as options.
248 This is to discourage accidental use of multiple inheritence, if you need this feature enabled, you should
249 probably be using Roles instead!
251 It is possible to change the default behavior of each parameter by passing a
252 C< $param_name.'_resolve_strategy' > parameter when currying a class generator, with values of either
253 C<merge> or C<replace>.
257 package My::ComponentGenerator;
260 with 'CatalystX::DynamicComponent' => {
261 name => 'generate_magic_component',
262 roles => ['My::Role'],
263 roles_resolve_strategy => 'replace',
269 My::ComponentGenerator
272 after 'setup_components' => sub {
274 # Component generated has no roles
275 $app->generate_magic_component('MyApp::Controller::Foo', { roles => [] });
276 # Component generated does My::Role
277 $app->generate_magic_component('MyApp::Controller::Foo', {} );
291 Test pre_immutable hook in tests
299 Unlame needing to pass fully qualified component name in, that's retarded...
301 Remember to fix the docs and clients too ;)
305 Tests for roles giving advice to methods which have just been added..
311 L<Catalyst>, L<MooseX::MethodAttributes>, L<CatalystX::ModelsFromConfig>.
315 Probably plenty, test suite certainly isn't comprehensive.. Patches welcome.
319 Tomas Doran (t0m) <bobtfish@bobtfish.net>
323 This code is copyright (c) 2009 Tomas Doran. This code is licensed on the same terms as perl