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) = @_;
88 my $appclass = blessed($app) || $app;
92 $name = $appclass . '::' . $name;
94 my $meta = Moose->init_meta( for_class => $name );
96 my @superclasses = @{ $get_resolved_config->('superclasses', $p, $config) };
97 push(@superclasses, 'Catalyst::' . $type) unless @superclasses;
98 $meta->superclasses(@superclasses);
100 my $methods = $get_resolved_config->('methods', $p, $config);
101 foreach my $name (keys %$methods) {
102 $meta->add_method($name => $methods->{$name});
105 if (my @roles = @{ $get_resolved_config->('roles', $p, $config) }) {
106 Moose::Util::apply_all_roles( $name, @roles);
109 if ($p->has_pre_immutable_hook) {
110 if (!ref($pre_immutable_hook)) {
111 $app->$pre_immutable_hook($meta, $config);
114 $pre_immutable_hook->($meta, $config);
118 $meta->make_immutable;
120 my $instance = $app->setup_component($name);
121 $app->components->{ $name } = $instance;
131 CatalystX::DynamicComponent - Parameterised Moose role providing functionality to build Catalyst components at runtime.
135 package My::DynamicComponentType;
137 use namespace::autoclean;
139 with 'CatalystX::DynamicComponent' => {
140 name => '_setup_one_of_my_components', # Name of injected method
143 after setup_components => sub { shift->_setup_all_my_components(@_); };
145 sub _setup_all_my_components {
147 my $app = ref($self) || $self;
148 foreach my $component_name ('Controller::Foo') {
149 my %component_config = %{ $c->config->{$component_name} };
150 # Shallow copy so we avoid stuffing methods back in the config, as that's lame!
151 $component_config{methods} = {
152 some_method => sub { 'foo' },
155 # Calling this method creates a component, and registers it in your application
156 # This component will subclass 'MyApp::ControllerBase', do 'MyApp::ControllerRole'
157 # and have a method called 'some_method' which will return the value 'foo'..
158 $self->_setup_one_of_my_components($app . '::' . $component_name, \%component_config);
164 use namespace::autoclean;
166 +My::DynameComponentType
170 'Controller::Foo' => {
171 superclasses => [qw/MyApp::ControllerBase/],
172 roles => [qw/MyApp::ControllerRole/],
179 CatalystX::DynamicComponent aims to provide a flexible and reuseable method of building L<Roles|Moose::Role>
180 which can be added to L<Catalyst> applications, which generate components dynamically at application
181 startup using the L<Moose> meta model.
183 Thi is implemented as a parametrised role which curries a
184 component builder method into your current package at application time.
186 Authors of specific dynamic component builders are expected implement an application class
187 roles which composes this role, and their own advice after the C<< setup_compontents >>
188 method, which will call the component generation method provided by using this role once
189 for each component you wish to create.
195 B<Required> - The name of the component generator method to curry.
199 Optional, a hash reference with keys being method names, and values being a L<Class::MOP::Method>,
200 or a plain code ref of a method to apply to
201 the dynamically generated package before making it immutable.
205 Optional, an array reference of roles to apply to the generated component
209 Optional, an array reference of superclasses to give the generated component.
211 If this is not defined, and not passed in as an argument to the generation method,
212 then Catalyst::(Model|View|Controller) will used as the base class (as appropriate given
213 the requested namespace of the generated class, otherwise Catalyst::Component will be used.
215 FIXME - Need tests for this.
217 =head2 pre_immutable_hook
219 Optional, either a coderef, which will be called with the component $meta and the merged $config,
220 or a string name of a method to call on the application class, with the same parameters.
222 This hook is called after a component has been generated and methods added, but before it is made
223 immutable, constructed, and added to your component registry.
225 =head1 CURRIED COMPONENT GENERATOR
233 $component_name (E.g. C<< MyApp::Controller::Foo >>)
237 $config (E.g. C<< $c->config->{$component_name} >>)
243 It is possible to set each of the roles, methods and superclasses parameters for each generated package
244 individually by defining those keys in the C< $config > parameter to your curried component generation method.
246 By default, roles and methods supplied from the curried role, and those passed as config will be merged.
248 Superclasses, no the other hand, will replace those from the curried configuration if passed as options.
249 This is to discourage accidental use of multiple inheritence, if you need this feature enabled, you should
250 probably be using Roles instead!
252 It is possible to change the default behavior of each parameter by passing a
253 C< $param_name.'_resolve_strategy' > parameter when currying a class generator, with values of either
254 C<merge> or C<replace>.
258 package My::ComponentGenerator;
261 with 'CatalystX::DynamicComponent' => {
262 name => 'generate_magic_component',
263 roles => ['My::Role'],
264 roles_resolve_strategy => 'replace',
270 My::ComponentGenerator
273 after 'setup_components' => sub {
275 # Component generated has no roles
276 $app->generate_magic_component('MyApp::Controller::Foo', { roles => [] });
277 # Component generated does My::Role
278 $app->generate_magic_component('MyApp::Controller::Foo', {} );
292 Test pre_immutable hook in tests
300 Unlame needing to pass fully qualified component name in, that's retarded...
302 Remember to fix the docs and clients too ;)
306 Tests for roles giving advice to methods which have just been added..
312 L<Catalyst>, L<MooseX::MethodAttributes>, L<CatalystX::ModelsFromConfig>.
316 Probably plenty, test suite certainly isn't comprehensive.. Patches welcome.
320 Tomas Doran (t0m) <bobtfish@bobtfish.net>
324 This code is copyright (c) 2009 Tomas Doran. This code is licensed on the same terms as perl