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 # stash the config of this generated class away
118 $meta->add_method('config', sub { return $app->config->{$name} });
120 $meta->make_immutable;
122 my $instance = $app->setup_component($component_name);
123 $app->components->{ $component_name } = $instance;
133 CatalystX::DynamicComponent - Parameterised Moose role providing functionality to build Catalyst components at runtime.
137 package My::DynamicComponentType;
139 use namespace::autoclean;
141 with 'CatalystX::DynamicComponent' => {
142 name => '_setup_one_of_my_components', # Name of injected method
145 after setup_components => sub { shift->_setup_all_my_components(@_); };
147 sub _setup_all_my_components {
149 my $app = ref($self) || $self;
150 foreach my $component_name ('Controller::Foo') {
151 my %component_config = %{ $c->config->{$component_name} };
152 # Shallow copy so we avoid stuffing methods back in the config, as that's lame!
153 $component_config{methods} = {
154 some_method => sub { 'foo' },
157 # Calling this method creates a component, and registers it in your application
158 # This component will subclass 'MyApp::ControllerBase', do 'MyApp::ControllerRole'
159 # and have a method called 'some_method' which will return the value 'foo'..
160 $self->_setup_one_of_my_components($app . '::' . $component_name, \%component_config);
166 use namespace::autoclean;
168 +My::DynameComponentType
172 'Controller::Foo' => {
173 superclasses => [qw/MyApp::ControllerBase/],
174 roles => [qw/MyApp::ControllerRole/],
181 CatalystX::DynamicComponent aims to provide a flexible and reuseable method of building L<Roles|Moose::Role>
182 which can be added to L<Catalyst> applications, which generate components dynamically at application
183 startup using the L<Moose> meta model.
185 Thi is implemented as a parametrised role which curries a
186 component builder method into your current package at application time.
188 Authors of specific dynamic component builders are expected implement an application class
189 roles which composes this role, and their own advice after the C<< setup_compontents >>
190 method, which will call the component generation method provided by using this role once
191 for each component you wish to create.
197 B<Required> - The name of the component generator method to curry.
201 Optional, a hash reference with keys being method names, and values being a L<Class::MOP::Method>,
202 or a plain code ref of a method to apply to
203 the dynamically generated package before making it immutable.
207 Optional, an array reference of roles to apply to the generated component
211 Optional, an array reference of superclasses to give the generated component.
213 If this is not defined, and not passed in as an argument to the generation method,
214 then Catalyst::(Model|View|Controller) will used as the base class (as appropriate given
215 the requested namespace of the generated class, otherwise Catalyst::Component will be used.
217 FIXME - Need tests for this.
219 =head2 pre_immutable_hook
221 Optional, either a coderef, which will be called with the component $meta and the merged $config,
222 or a string name of a method to call on the application class, with the same parameters.
224 This hook is called after a component has been generated and methods added, but before it is made
225 immutable, constructed, and added to your component registry.
227 =head1 CURRIED COMPONENT GENERATOR
235 $component_name (E.g. C<< MyApp::Controller::Foo >>)
239 $config (E.g. C<< $c->config->{$component_name} >>)
245 It is possible to set each of the roles, methods and superclasses parameters for each generated package
246 individually by defining those keys in the C< $config > parameter to your curried component generation method.
248 By default, roles and methods supplied from the curried role, and those passed as config will be merged.
250 Superclasses, no the other hand, will replace those from the curried configuration if passed as options.
251 This is to discourage accidental use of multiple inheritence, if you need this feature enabled, you should
252 probably be using Roles instead!
254 It is possible to change the default behavior of each parameter by passing a
255 C< $param_name.'_resolve_strategy' > parameter when currying a class generator, with values of either
256 C<merge> or C<replace>.
260 package My::ComponentGenerator;
263 with 'CatalystX::DynamicComponent' => {
264 name => 'generate_magic_component',
265 roles => ['My::Role'],
266 roles_resolve_strategy => 'replace',
272 My::ComponentGenerator
275 after 'setup_components' => sub {
277 # Component generated has no roles
278 $app->generate_magic_component('MyApp::Controller::Foo', { roles => [] });
279 # Component generated does My::Role
280 $app->generate_magic_component('MyApp::Controller::Foo', {} );
294 Test pre_immutable hook in tests
302 Unlame needing to pass fully qualified component name in, that's retarded...
304 Remember to fix the docs and clients too ;)
308 Tests for roles giving advice to methods which have just been added..
314 L<Catalyst>, L<MooseX::MethodAttributes>, L<CatalystX::ModelsFromConfig>.
318 Probably plenty, test suite certainly isn't comprehensive.. Patches welcome.
322 Tomas Doran (t0m) <bobtfish@bobtfish.net>
326 This code is copyright (c) 2009 Tomas Doran. This code is licensed on the same terms as perl