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/;
8 use namespace::autoclean;
10 enum __PACKAGE__ . '::ResolveStrategy' => qw/
15 our $VERSION = 0.000001;
22 parameter 'pre_immutable_hook' => (
24 predicate => 'has_pre_immutable_hook',
27 my $coerceablearray = subtype ArrayRef;
28 coerce $coerceablearray, from Str, via { [ $_ ] };
33 default => sub { {} },
34 resolve_strategy => 'merge',
37 isa => $coerceablearray, coerce => 1,
38 default => sub { [] },
39 resolve_strategy => 'merge',
42 isa => $coerceablearray, coerce => 1,
43 default => sub { [] },
44 resolve_strategy => 'replace',
48 # Shameless metaprogramming.
49 foreach my $name (keys %parameters) {
50 my $resolve_strategy = delete $parameters{$name}->{resolve_strategy};
52 parameter $name, %{ $parameters{$name} };
54 parameter $name . '_resolve_strategy' => (
55 isa => __PACKAGE__ . '::ResolveStrategy',
56 default => $resolve_strategy,
60 # Code refs to implement the strategy types
61 my %strategies = ( # Right hand precedence where appropriate
63 $_[0] = [ $_[0] ] if $_[0] && !ref $_[0];
64 $_[1] = [ $_[1] ] if $_[1] && !ref $_[1];
65 $_[1] ? $_[1] : $_[0];
68 $_[0] = [ $_[0] ] if $_[0] && !ref $_[0];
69 $_[1] = [ $_[1] ] if $_[1] && !ref $_[1];
70 if (ref($_[0]) eq 'ARRAY' || ref($_[1]) eq 'ARRAY') {
71 [ uniq( @{ $_[0] }, @{ $_[1] } ) ];
74 Catalyst::Utils::merge_hashes(shift, shift);
79 # Wrap all the crazy up in a method to generically merge configs.
80 my $get_resolved_config = sub {
81 my ($name, $p, $config) = @_;
82 my $get_strategy_method_name = $name . '_resolve_strategy';
83 my $strategy = $strategies{$p->$get_strategy_method_name()};
84 $strategy->($p->$name, $config->{$name})
85 || $parameters{$name}->{default}->();
91 my $pre_immutable_hook = $p->pre_immutable_hook;
94 my ($app, $name, $config) = @_;
95 my $appclass = blessed($app) || $app;
102 my $component_name = $appclass . '::' . $name;
103 my $meta = Moose->init_meta( for_class => $component_name );
105 my @superclasses = @{ $get_resolved_config->('superclasses', $p, $config) };
106 push(@superclasses, 'Catalyst::' . $type) unless @superclasses;
107 $meta->superclasses(@superclasses);
109 my $methods = $get_resolved_config->('methods', $p, $config);
110 foreach my $method_name (keys %$methods) {
111 $meta->add_method($method_name => $methods->{$method_name});
114 if (my @roles = @{ $get_resolved_config->('roles', $p, $config) }) {
115 Moose::Util::apply_all_roles( $component_name, @roles);
118 if ($p->has_pre_immutable_hook) {
119 if (!ref($pre_immutable_hook)) {
120 $app->$pre_immutable_hook($meta, $config);
123 $pre_immutable_hook->($meta, $config);
127 $meta->make_immutable;
129 my $instance = $app->setup_component($component_name);
130 $app->components->{ $component_name } = $instance;
140 CatalystX::DynamicComponent - Parameterised Moose role providing functionality to build Catalyst components at runtime.
144 package My::DynamicComponentType;
146 use namespace::autoclean;
148 with 'CatalystX::DynamicComponent' => {
149 name => '_setup_one_of_my_components', # Name of injected method
152 after setup_components => sub { shift->_setup_all_my_components(@_); };
154 sub _setup_all_my_components {
156 my $app = ref($self) || $self;
157 foreach my $component_name ('Controller::Foo') {
158 my %component_config = %{ $c->config->{$component_name} };
159 # Shallow copy so we avoid stuffing methods back in the config, as that's lame!
160 $component_config{methods} = {
161 some_method => sub { 'foo' },
164 # Calling this method creates a component, and registers it in your application
165 # This component will subclass 'MyApp::ControllerBase', do 'MyApp::ControllerRole'
166 # and have a method called 'some_method' which will return the value 'foo'..
167 $self->_setup_one_of_my_components($app . '::' . $component_name, \%component_config);
173 use namespace::autoclean;
175 +My::DynameComponentType
179 'Controller::Foo' => {
180 superclasses => [qw/MyApp::ControllerBase/],
181 roles => [qw/MyApp::ControllerRole/],
188 CatalystX::DynamicComponent aims to provide a flexible and reuseable method of building L<Roles|Moose::Role>
189 which can be added to L<Catalyst> applications, which generate components dynamically at application
190 startup using the L<Moose> meta model.
192 Thi is implemented as a parametrised role which curries a
193 component builder method into your current package at application time.
195 Authors of specific dynamic component builders are expected implement an application class
196 roles which composes this role, and their own advice after the C<< setup_compontents >>
197 method, which will call the component generation method provided by using this role once
198 for each component you wish to create.
204 B<Required> - The name of the component generator method to curry.
208 Optional, a hash reference with keys being method names, and values being a L<Class::MOP::Method>,
209 or a plain code ref of a method to apply to
210 the dynamically generated package before making it immutable.
214 Optional, an array reference of roles to apply to the generated component
218 Optional, an array reference of superclasses to give the generated component.
220 If this is not defined, and not passed in as an argument to the generation method,
221 then Catalyst::(Model|View|Controller) will used as the base class (as appropriate given
222 the requested namespace of the generated class, otherwise Catalyst::Component will be used.
224 FIXME - Need tests for this.
226 =head2 pre_immutable_hook
228 Optional, either a coderef, which will be called with the component $meta and the merged $config,
229 or a string name of a method to call on the application class, with the same parameters.
231 This hook is called after a component has been generated and methods added, but before it is made
232 immutable, constructed, and added to your component registry.
234 =head1 CURRIED COMPONENT GENERATOR
242 $component_name (E.g. C<< MyApp::Controller::Foo >>)
246 $config (E.g. C<< $c->config->{$component_name} >>)
252 It is possible to set each of the roles, methods and superclasses parameters for each generated package
253 individually by defining those keys in the C< $config > parameter to your curried component generation method.
255 By default, roles and methods supplied from the curried role, and those passed as config will be merged.
257 Superclasses, no the other hand, will replace those from the curried configuration if passed as options.
258 This is to discourage accidental use of multiple inheritence, if you need this feature enabled, you should
259 probably be using Roles instead!
261 It is possible to change the default behavior of each parameter by passing a
262 C< $param_name.'_resolve_strategy' > parameter when currying a class generator, with values of either
263 C<merge> or C<replace>.
267 package My::ComponentGenerator;
270 with 'CatalystX::DynamicComponent' => {
271 name => 'generate_magic_component',
272 roles => ['My::Role'],
273 roles_resolve_strategy => 'replace',
279 My::ComponentGenerator
282 after 'setup_components' => sub {
284 # Component generated has no roles
285 $app->generate_magic_component('MyApp::Controller::Foo', { roles => [] });
286 # Component generated does My::Role
287 $app->generate_magic_component('MyApp::Controller::Foo', {} );
301 Test pre_immutable hook in tests
309 Unlame needing to pass fully qualified component name in, that's retarded...
311 Remember to fix the docs and clients too ;)
315 Tests for roles giving advice to methods which have just been added..
321 L<Catalyst>, L<MooseX::MethodAttributes>, L<CatalystX::ModelsFromConfig>.
325 Probably plenty, test suite certainly isn't comprehensive.. Patches welcome.
329 Tomas Doran (t0m) <bobtfish@bobtfish.net>
333 This code is copyright (c) 2009 Tomas Doran. This code is licensed on the same terms as perl