File Coverage

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


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