File Coverage

blib/lib/HTML/FormFu/Model/HashRef.pm
Criterion Covered Total %
statement 173 181 95.5
branch 65 78 83.3
condition 23 29 79.3
subroutine 16 17 94.1
pod 3 5 60.0
total 280 310 90.3


line stmt bran cond sub pod time code
1             package HTML::FormFu::Model::HashRef;
2              
3 6     6   720 use strict;
  6         9  
  6         295  
4             our $VERSION = '2.05'; # VERSION
5              
6 6     6   21 use Moose;
  6         7  
  6         39  
7 6     6   28087 use MooseX::Attribute::FormFuChained;
  6         12  
  6         189  
8              
9             extends 'HTML::FormFu::Model';
10              
11 6     6   2928 use Hash::Flatten;
  6         11040  
  6         271  
12 6     6   35 use Scalar::Util qw(blessed);
  6         8  
  6         9492  
13              
14             has flatten => ( is => 'rw' );
15             has options => ( is => 'rw' );
16              
17             has _repeatable => ( is => 'rw', traits => ['FormFuChained'] );
18             has _multi => ( is => 'rw', traits => ['FormFuChained'] );
19              
20             has deflators => (
21             is => 'rw',
22             default => 1,
23             lazy => 1,
24             traits => ['FormFuChained'],
25             );
26              
27             has inflators => (
28             is => 'rw',
29             default => 1,
30             lazy => 1,
31             traits => ['FormFuChained'],
32             );
33              
34             sub default_values {
35 8     8 1 17 my ( $self, $data ) = @_;
36 94         172 map { $_->default(undef) }
37 8         14 ( grep { $_->is_field } @{ $self->form->get_all_elements } );
  134         2691  
  8         35  
38 8         35 $self->_default_values( $self->form, $data );
39 8         46 return $self;
40             }
41              
42             sub _default_values {
43 51     51   54 my ( $self, $form, $data ) = @_;
44 51         148 my $elements = $form->get_elements;
45 51         64 foreach my $element ( @{$elements} ) {
  51         64  
46 113   100     267 my $name = $element->name || "";
47 113   100     1042 my $nested_name = $element->nested_name || "";
48 113 100       198 $name =~ s/_\d+$// if ($name);
49 113 100 100     2468 if ( $element->is_repeatable ) {
    100          
    100          
50 7   33     28 my $value = $data->{$name} || $data->{$nested_name};
51 7 100       21 unless ($value) {
52 1         6 $element->repeat(0);
53 0         0 map { $element->remove_element($_) }
54 1         2 @{ $element->get_elements };
  1         5  
55 1         3 next;
56             }
57 6         9 my $k = scalar @{$value};
  6         15  
58 6         30 $element->repeat($k);
59 6         33 my $childs = $element->get_elements;
60 6         25 for ( my $i = 0; $i < $k; $i++ ) {
61 11         54 $self->_default_values( $childs->[$i], $value->[$i] );
62             }
63             }
64             elsif ( $element->is_block && $element->is_field )
65             { # is a Multi element
66             ref $data->{$name} eq "HASH"
67             ? $self->_default_values( $element, $data->{$name} )
68 14 100       72 : $element->default( $data->{$name} );
69             }
70             elsif ( $element->is_block ) {
71             $self->_default_values( $element,
72             $nested_name
73 28 100       108 ? $data->{$nested_name}
74             : $data );
75             }
76             else {
77 64 100 66     1409 if ( $self->inflators && @{ $element->get_inflators } > 0 ) {
  64         154  
78 4         7 my @inflators = @{ $element->get_inflators };
  4         13  
79 4         12 map { $element->default( $_->process( $data->{$name} ) ) }
  4         44  
80             @inflators;
81             }
82             else {
83              
84 60         164 $element->default( $data->{$name} );
85             }
86             }
87              
88             }
89              
90 51         94 return $self;
91              
92             }
93              
94 0     0 1 0 sub update { shift->create(@_) }
95              
96             sub create {
97 8     8 1 14 my $self = shift;
98 8 100       33 if ( $self->form->submitted ) {
99 2         8 my $input = _escape_hash( $self->form->input );
100 2         19 my $hf = Hash::Flatten->new(
101             { ArrayDelimiter => '_', HashDelimiter => '.' } );
102 2         92 $input = _unescape_hash( $hf->unflatten( $self->form->input ) );
103 2         8 $self->default_values(
104             $self->_unfold_repeatable( $self->form, $input ) );
105             }
106 8         31 $self->form->render_data;
107 8         37 my $obj = $self->_as_object_get( $self->form );
108 7 100       192 if ( $self->flatten ) {
109 1         7 my $hf = Hash::Flatten->new(
110             { ArrayDelimiter => '_', HashDelimiter => '.' } );
111 1         18 $obj = $self->_unfold_repeatable( $self->form, $hf->flatten($obj) );
112             }
113 7         147 return $obj;
114             }
115              
116             sub _as_object_get {
117 8     8   13 my $self = shift;
118 8         12 my $form = shift;
119 8         38 my $e = $form->get_all_elements;
120 8         19 my $names = {};
121 8         13 foreach my $element ( @{$e} ) {
  8         18  
122 156         1322 my $name = $element->nested_name;
123 156 100       240 next unless $name;
124 130 100       2631 next if ( $element->type eq "Multi" );
125 120         173 my $es_name = _escape_name($name);
126 120 100 100     2640 if ( $self->options
    100 66        
      66        
127             && $element->can('_options')
128 22         500 && @{ $element->_options } > 0 )
129             {
130 22         19 my @options = @{ $element->_options };
  22         500  
131             my @values
132             = ref $element->default eq "ARRAY"
133 22 100       52 ? @{ $element->default }
  2         5  
134             : $element->default;
135 22         51 $names->{$es_name} = [];
136 22         29 foreach my $value (@values) {
137             my @option
138 24 100       29 = grep { defined $value && $_->{value} eq $value } @options;
  402         809  
139 24 100       40 unless (@option) {
140 8 50       9 @options = map { @{ $_->{group} || [] } } @options;
  134         65  
  134         302  
141 8         14 @option = grep { $_->{value} eq $value } @options;
  0         0  
142             }
143             my $obj
144 24         37 = [ map { { value => $_->{value}, label => $_->{label} } }
  16         50  
145             @option ];
146              
147 24 50       38 push( @{ $names->{$es_name} }, $obj->[0] ) if $name;
  24         54  
148             }
149 22 100       51 $names->{$es_name} = $names->{$es_name}->[0] if scalar @values == 1;
150 22   100     64 $names->{$es_name} ||= { value => undef, label => undef };
151             }
152             elsif ( $element->is_field && $self->deflators ) {
153 77         194 my $deflators = $element->get_deflators;
154 77 50       435 $names->{$es_name} = $element->default
155             if ( $element->can('default') );
156 4         20 map { $names->{$es_name} = $_->deflator( $names->{$es_name} ) }
157 77         66 @{$deflators};
  77         90  
158             }
159             else {
160 21 50       87 $names->{$es_name} = $element->default
161             if ( $element->can('default') );
162             }
163              
164 120 50       377 if ( blessed $names->{$es_name} ) { delete $names->{$es_name} }
  0         0  
165             }
166              
167 8         63 my $hf = Hash::Flatten->new( { ArrayDelimiter => '_' } );
168              
169 8 100       468 return $self->_unfold_repeatable( $form,
170             $self->flatten ? $names : $hf->unflatten($names) );
171             }
172              
173             sub _escape_hash {
174 15     15   18 my $hash = shift;
175 15   100     33 my $method = shift || \&_escape_name;
176 15 100       28 return $hash unless ( ref $hash );
177 14         31 foreach my $k ( keys %$hash ) {
178 31         33 my $v = delete $hash->{$k};
179 31 100       55 if ( ref $v eq 'HASH' ) {
    100          
180 4         12 $hash->{ $method->($k) } = _escape_hash( $v, $method );
181             }
182             elsif ( ref $v eq 'ARRAY' ) {
183             $hash->{ $method->($k) }
184 3         5 = [ map { _escape_hash( $_, $method ) } @$v ];
  5         8  
185             }
186             else {
187 24         30 $hash->{ $method->($k) } = $v;
188             }
189             }
190 14         40 return $hash;
191             }
192              
193             sub _unescape_hash {
194 3     3   266 return _escape_hash( shift, \&_unescape_name );
195             }
196              
197             sub _escape_name {
198 140     140   1729 my $name = shift;
199 140         234 $name =~ s/_/\\_/g;
200 140         255 $name =~ s/\\(_\d+(\.|$))/$1/g;
201 140         182 return $name;
202             }
203              
204             sub _unescape_name {
205 184     184   566 my $name = shift;
206 184         209 $name =~ s/\\_/_/g;
207 184         137 $name =~ s/\\\./\./g;
208 184         181 return $name;
209             }
210              
211             sub _unfold_repeatable {
212 193     193   3862 my $self = shift;
213 193         129 my $form = shift;
214 193         147 my $data = shift;
215 193 100       593 return $data unless ( ref $data eq "HASH" );
216 42         43 my $new = {};
217              
218 42         39 while ( my ( $k, $v ) = each %{$data} ) {
  209         427  
219 168         181 my $key = _unescape_name($k);
220              
221 168 100 66     193 if ( $self->get_repeatable($key) ) {
    50          
222 6         15 $new->{$key} = [];
223              
224             # iterate over all array elements
225             # we ignore the first one (index 0) as it is undef as we start
226             # counting the repeated element names with 1 and the automatic
227             # from Hash::Flatten assumed 0 as first index while unflattening
228             # the parameter names
229             # Example:
230             # $v = [
231             # undef,
232             # {
233             # 'foo' => 'bar',
234             # 'id' => 1
235             # },
236             # {
237             # 'foo' => 'baz',
238             # 'id' => 2
239             # }
240             # ];
241 6 50       12 for ( my $i = 1; $i < @{ $v || [] }; $i++ ) {
  17         52  
242              
243             # process all key value pairs in an array element
244 11         13 while ( my ( $name, $values ) = each %{ $v->[$i] } ) {
  32         74  
245              
246             # add an empty hash to array of unfolded data if not already present
247 11         19 push( @{ $new->{$key} }, {} )
248 21 100       45 unless $new->{$key}->[ $i - 1 ];
249              
250             # store processed values
251 21         39 $new->{$key}->[ $i - 1 ]->{$name}
252             = $self->_unfold_repeatable( $form, $values );
253             }
254             }
255             }
256             elsif ( $self->get_multi($key) && ref $v eq "ARRAY" ) {
257 0 0       0 for ( @{ $v || [] } ) {
  0         0  
258 0         0 $new->{$key} = $_;
259 0 0       0 last if $new->{$key};
260             }
261             }
262             else {
263 161         193 $new->{$key} = $self->_unfold_repeatable( $form, $v );
264             }
265             }
266              
267 41         130 return $new;
268             }
269              
270             sub get_multi {
271 161     161 0 120 my $self = shift;
272 161         123 my $element = shift;
273 161 100       3409 unless ( $self->_multi ) {
274 4         13 my %multis = ();
275 4         16 my $multis = $self->form->get_all_elements( { type => qr/Multi/ } );
276 4 50       13 foreach my $multi ( @{ $multis || [] } ) {
  4         21  
277 4         5 my @multis;
278 4         3 map { push( @multis, $_->name ) } @{ $multi->get_elements };
  8         15  
  4         13  
279 4         7 map { my $i = $_; $i =~ s/_\d+//; $multis{$i} = 1 } @multis;
  8         9  
  8         7  
  8         14  
280             }
281 4         102 $self->_multi( \%multis );
282             }
283 161         3295 return $self->_multi->{$element};
284              
285             }
286              
287             sub get_repeatable {
288 168     168 0 128 my $self = shift;
289 168         101 my $element = shift;
290 168 100       3803 unless ( $self->_repeatable ) {
291 5         11 my %rep = ();
292 5         20 my $rep = $self->form->get_all_elements( { type => qr/Repeatable/ } );
293              
294             # TODO - Mario Minati 19.05.2009
295             # use $_->delimiter to split the keys
296 5 50       18 foreach my $rep_element ( @{ $rep || [] } ) {
  5         23  
297 4         100 my $name = $rep_element->nested_name;
298 4 100       29 die
299             "A Repeatable element without a nested_name attribute cannot be handled by Model::HashRef"
300             unless $name;
301 3         8 $name =~ s/_\d+//;
302 3         7 $rep{$name} = 1;
303             }
304 4         108 $self->_repeatable( \%rep );
305             }
306 167         3566 return $self->_repeatable->{$element};
307              
308             }
309              
310             __PACKAGE__->meta->make_immutable;
311              
312             __END__
313              
314             =head1 NAME
315              
316             HTML::FormFu::Model::HashRef - handle hashrefs
317              
318             =head1 VERSION
319              
320             version 2.05
321              
322             =head1 SYNOPSIS
323              
324             ---
325             elements:
326             - user_id
327             - user_name
328             - type: Repeatable
329             nested_name: addresses
330             elements:
331             - type: Hidden
332             name: id
333             - street
334              
335              
336             $form->model('HashRef')->default_values( {
337             user_id => 123,
338             user_name => 'Hans',
339             addresses => [
340             { id => 2,
341             street => 'Somewhere' },
342             { id => 3,
343             street => 'Somewhere Else' }
344             ]
345             } );
346              
347             $form->default_model('HashRef');
348             my $hashref = $form->model->create();
349              
350             # $hashref is very much the same as the hashref you passed to default_values()
351              
352             =head1 DESCRIPTION
353              
354             If you need the content of a formular as hashref or for processing with other modules
355             like C<JSON> you can use this model.
356              
357             =head1 METHODS
358              
359             =head2 create
360              
361             This method creates a hashref from a filled form. This form can be filled by calling
362             L<HTML::FormFu/default_values>, default_values of any other model class (e. g. L<HTML::FormFu::Model::DBIC>)
363             or by simply submitting the form.
364              
365             If L</deflators> is true all deflators are processed (defaults to C<1>).
366              
367             If L</options> is true the value of all elements which have options like
368             L<HTML::FormFu::Element::Select> will be transformed.
369              
370             ---
371             elements:
372             - type: Select
373             name: select
374             options:
375             - [1, "Foo"]
376             - [2, "Bar"]
377              
378             If the value of C<select> is C<1>, L<create> will create this hashref:
379              
380             { 'select' => { label => 'Foo', value => 1 } }
381              
382             If there is more than one value selected, an arrayref is created instead:
383              
384             { 'select' => [ { label => 'Foo', value => 1 },
385             { label => 'Bar', value => 2 } ] }
386              
387             If L</options> is false, the output will look like this:
388              
389             { 'select' => 1 }
390              
391             respectively
392              
393             { 'select' => [1, 2] }
394              
395             L</options> is false by default.
396              
397             To get a flattened hash, you can set C</flatten> to a true value (defaults to C<0>).
398             This will generate a hash which uses the nested name of each field as key and the value
399             of this field as hash value. If there is a field which has more than one value,
400             a counter is added. The above example would result in a hash like this using C</flatten>:
401              
402             { 'select_0' => 1,
403             'select_1' => 2 }
404              
405              
406             =head2 update
407              
408             Alias for L</create>.
409              
410             =head2 default_values
411              
412             Populate a form using a hashref. This hashref has the same format as the output of L</create>.
413             If L</inflators> is true, all inflators will be processed (defaults to C<1>).
414              
415             =head1 CONFIGURATION
416              
417             These methods do not return the model object so chaining is not possible!
418              
419             =head2 options
420              
421             Adds the label of a value to the hashref if the element has L<HTML::FormFu::Role::Element::Group/options>.
422             See L</create> for an example. Defaults to C<0>.
423              
424             =head2 flatten
425              
426             Flattens the hash using L<Hash::Flatten>. See L</create> for an example. Defaults to C<0>.
427              
428             =head2 deflators
429              
430             If true, processes deflators in C</create>. Defaults to C<1>.
431              
432             =head2 inflators
433              
434             If true, processes inflators in C</default_values>. Defaults to C<1>.
435              
436             =head1 SEE ALSO
437              
438             L<HTML::FormFu>, L<Hash::Flatten>
439              
440             =head1 AUTHOR
441              
442             Moritz Onken, C<< onken@houseofdesign.de >>