File: HashRef.pm

package info (click to toggle)
libhtml-formfu-perl 0.09007-1
  • links: PTS, VCS
  • area: main
  • in suites: wheezy
  • size: 4,184 kB
  • sloc: perl: 13,186; makefile: 9; sql: 5
file content (428 lines) | stat: -rw-r--r-- 12,423 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
package HTML::FormFu::Model::HashRef;
use Moose;
use MooseX::Attribute::Chained;

extends 'HTML::FormFu::Model';

use Hash::Flatten;
use Scalar::Util qw(blessed);

has flatten => ( is => 'rw' );
has options => ( is => 'rw' );

has _repeatable => ( is => 'rw', traits  => ['Chained'] );
has _multi      => ( is => 'rw', traits  => ['Chained'] );

has deflators => (
    is      => 'rw',
    default => 1,
    lazy    => 1,
    traits  => ['Chained'],
);

has inflators => (
    is      => 'rw',
    default => 1,
    lazy    => 1,
    traits  => ['Chained'],
);

sub default_values {
    my ( $self, $data ) = @_;
    map { $_->default(undef) }
        ( grep { $_->is_field } @{ $self->form->get_all_elements } );
    $self->_default_values( $self->form, $data );
    return $self;
}

sub _default_values {
    my ( $self, $form, $data ) = @_;
    my $elements = $form->get_elements;
    foreach my $element ( @{$elements} ) {
        my $name        = $element->name        || "";
        my $nested_name = $element->nested_name || "";
        $name =~ s/_\d+$// if ($name);
        if ( $element->is_repeatable ) {
            my $value = $data->{$name} || $data->{$nested_name};
            unless ($value) {
                $element->repeat(0);
                map { $element->remove_element($_) }
                    @{ $element->get_elements };
                next;
            }
            my $k = scalar @{$value};
            $element->repeat($k);
            my $childs = $element->get_elements;
            for ( my $i = 0; $i < $k; $i++ ) {
                $self->_default_values( $childs->[$i], $value->[$i] );
            }
        }
        elsif ( $element->is_block && $element->is_field )
        {    # is a Multi element
            ref $data->{$name} eq "HASH"
                ? $self->_default_values( $element, $data->{$name} )
                : $element->default( $data->{$name} );
        }
        elsif ( $element->is_block ) {
            $self->_default_values( $element,
                  $nested_name
                ? $data->{$nested_name}
                : $data );
        }
        else {
            if ( $self->inflators && @{ $element->get_inflators } > 0 ) {
                my @inflators = @{ $element->get_inflators };
                map { $element->default( $_->process( $data->{$name} ) ) }
                    @inflators;
            }
            else {

                $element->default( $data->{$name} );
            }
        }

    }

    return $self;

}

sub update { shift->create(@_) }

sub create {
    my $self = shift;
    if($self->form->submitted) {
        my $input = _escape_hash($self->form->input);
        my $hf = new Hash::Flatten(
            { ArrayDelimiter => '_', HashDelimiter => '.' } );
        $input = _unescape_hash($hf->unflatten($self->form->input));
        $self->default_values( $self->_unfold_repeatable($self->form, $input) );
    }
    $self->form->render_data;
    my $obj = $self->_as_object_get( $self->form );
    if ( $self->flatten ) {
        my $hf = new Hash::Flatten(
            { ArrayDelimiter => '_', HashDelimiter => '.' } );
        $obj = $self->_unfold_repeatable( $self->form, $hf->flatten($obj) );
    }
    return $obj;
}

sub _as_object_get {
    my $self  = shift;
    my $form  = shift;
    my $e     = $form->get_all_elements;
    my $names = {};
    foreach my $element ( @{$e} ) {
        my $name = $element->nested_name;
        next unless $name;
        next if ( $element->type eq "Multi" );
        my $es_name = _escape_name($name);
        if (   $self->options
            && $element->can('_options')
            && @{ $element->_options } > 0 )
        {
            my @options = @{ $element->_options };
            my @values
                = ref $element->default eq "ARRAY"
                ? @{ $element->default }
                : $element->default;
            $names->{$es_name} = [];
            foreach my $value (@values) {
                my @option
                    = grep { defined $value && $_->{value} eq $value } @options;
                unless (@option) {
                    @options = map { @{ $_->{group} || [] } } @options;
                    @option
                        = grep { $_->{value} eq $value } @options;
                }
                my $obj
                    = [ map { { value => $_->{value}, label => $_->{label} } }
                        @option ];

                push( @{ $names->{$es_name} }, $obj->[0] ) if $name;
            }
            $names->{$es_name} = $names->{$es_name}->[0] if scalar @values == 1;
            $names->{$es_name} ||= { value => undef, label => undef };
        }
        elsif ( $element->is_field && $self->deflators ) {
            my $deflators = $element->get_deflators;
            $names->{$es_name} = $element->default
                if ( $element->can('default') );
            map { $names->{$es_name} = $_->deflator( $names->{$es_name} ) }
                @{$deflators};
        }
        else {
            $names->{$es_name} = $element->default
                if ( $element->can('default') );
        }

        if ( blessed $names->{$es_name} ) { delete $names->{$es_name} }
    }

    my $hf = new Hash::Flatten( { ArrayDelimiter => '_' } );

    return $self->_unfold_repeatable( $form,
        $self->flatten ? $names : $hf->unflatten($names) );
}

sub _escape_hash {
    my $hash = shift;
    my $method = shift || \&_escape_name;
    return $hash unless(ref $hash);
    foreach my $k (keys %$hash) {
        my $v = delete $hash->{$k};
        if(ref $v eq 'HASH') {
            $hash->{$method->($k)} = _escape_hash($v, $method);
        } elsif(ref $v eq 'ARRAY') {
            $hash->{$method->($k)} = [ map { _escape_hash($_, $method) } @$v];
        } else {
            $hash->{$method->($k)} = $v;
        }
    }
    return $hash;
}


sub _unescape_hash {
    return _escape_hash(shift, \&_unescape_name);
}

sub _escape_name {
    my $name = shift;
    $name =~ s/_/\\_/g;
    $name =~ s/\\(_\d+(\.|$))/$1/g;
    return $name;
}

sub _unescape_name {
    my $name = shift;
    $name =~ s/\\_/_/g;
    $name =~ s/\\\./\./g;
    return $name;
}

sub _unfold_repeatable {
    my $self = shift;
    my $form = shift;
    my $data = shift;
    return $data unless ( ref $data eq "HASH" );
    my $new = {};

    while ( my ( $k, $v ) = each %{$data} ) {
        my $key = _unescape_name($k);

        if ( $self->get_repeatable($key) ) {
            $new->{$key} = [];
            # iterate over all array elements
            # we ignore the first one (index 0) as it is undef as we start
            # counting the repeated element names with 1 and the automatic
            # from Hash::Flatten assumed 0 as first index while unflattening
            # the parameter names
            # Example:
            # $v    = [
            #           undef,
            #           {
            #             'foo' => 'bar',
            #             'id' => 1
            #           },
            #           {
            #             'foo' => 'baz',
            #             'id' => 2
            #           }
            #         ];
            for ( my $i = 1; $i < @{$v || []}; $i++ ) {

                # process all key value pairs in an array element
                while ( my ( $name, $values ) = each %{$v->[$i]} ) {

                    # add an empty hash to array of unfolded data if not already present
                    push( @{ $new->{$key} }, {} ) unless $new->{$key}->[ $i - 1 ];

                    # store processed values
                    $new->{$key}->[ $i - 1 ]->{$name}
                        = $self->_unfold_repeatable( $form, $values );
                }
            }
        }
        elsif ( $self->get_multi($key) && ref $v eq "ARRAY" ) {
            for ( @{ $v || [] } ) {
                $new->{$key} = $_;
                last if $new->{$key};
            }
        }
        else {
            $new->{$key} = $self->_unfold_repeatable( $form, $v );
        }
    }

    return $new;
}

sub get_multi {
    my $self    = shift;
    my $element = shift;
    unless ( $self->_multi ) {
        my %multis = ();
        my $multis = $self->form->get_all_elements( { type => qr/Multi/ } );
        foreach my $multi ( @{ $multis || [] } ) {
            my @multis;
            map { push( @multis, $_->name ) } @{ $multi->get_elements };
            map { s/_\d+//; $multis{$_} = 1 } @multis;
        }
        $self->_multi( \%multis );
    }
    return $self->_multi->{$element};

}

sub get_repeatable {
    my $self    = shift;
    my $element = shift;
    unless ( $self->_repeatable ) {
        my %rep = ();
        my $rep = $self->form->get_all_elements( { type => qr/Repeatable/ } );
# TODO - Mario Minati 19.05.2009
# use $_->delimiter to split the keys
        foreach my $rep_element (@{ $rep || [] }) {
            my $name = $rep_element->nested_name;
            die "A Repeatable element without a nested_name attribute cannot be handled by Model::HashRef"
                unless $name;
            $name =~ s/_\d+//;
            $rep{$name} = 1;
        }
        $self->_repeatable( \%rep );
    }
    return $self->_repeatable->{$element};

}

__PACKAGE__->meta->make_immutable;

__END__

=head1 NAME

HTML::FormFu::Model::HashRef - handle hashrefs

=head1 SYNOPSIS

  ---
    elements:
      - user_id
      - user_name
      - type: Repeatable
        nested_name: addresses
        elements:
          - type: Hidden
            name: id
          - street


  $form->model('HashRef')->default_values( {
    user_id => 123,
    user_name => 'Hans',
    addresses => [
      { id => 2,
        street => 'Somewhere' },
      { id => 3,
        street => 'Somewhere Else' }
    ]    
    } );
  
  $form->default_model('HashRef');
  my $hashref = $form->model->create();
  
  # $hashref is very much the same as the hashref you passed to default_values()

=head1 DESCRIPTION

If you need the content of a formular as hashref or for processing with other modules 
like C<JSON> you can use this model.

=head1 METHODS

=head2 create

This method creates a hashref from a filled form. This form can be filled by calling
L<HTML::FormFu/default_values>, default_values of any other model class (e. g. L<HTML::FormFu::Model::DBIC>)
or by simply submitting the form.

If L</deflators> is true all deflators are processed (defaults to C<1>).

If L</options> is true the value of all elements which have options like 
L<HTML::FormFu::Element::Select> will be transformed.

  ---
    elements:
      - type: Select
        name: select
        options:
          - [1, "Foo"]
          - [2, "Bar"]

If the value of C<select> is C<1>, L<create> will create this hashref:

  { 'select' => { label => 'Foo', value => 1 } }

If there is more than one value selected, an arrayref is created instead:

  { 'select' => [ { label => 'Foo', value => 1 },
                  { label => 'Bar', value => 2 } ] }

If L</options> is false, the output will look like this:

  { 'select' => 1 }

respectively

  { 'select' => [1, 2] }

L</options> is false by default.

To get a flattened hash, you can set C</flatten> to a true value (defaults to C<0>).
This will generate a hash which uses the nested name of each field as key and the value
of this field as hash value. If there is a field which has more than one value,
a counter is added. The above example would result in a hash like this using C</flatten>:

  { 'select_0' => 1,
    'select_1' => 2 }


=head2 update

Alias for L</create>.

=head2 default_values

Populate a form using a hashref. This hashref has the same format as the output of L</create>.
If L</inflators> is true, all inflators will be processed (defaults to C<1>).

=head1 CONFIGURATION

These methods do not return the model object so chaining is not possible!

=head2 options

Adds the label of a value to the hashref if the element has L<HTML::FormFu::Element::_Group/options>.
See L</create> for an example. Defaults to C<0>.

=head2 flatten

Flattens the hash using L<Hash::Flatten>. See L</create> for an example. Defaults to C<0>.

=head2 deflators

If true, processes deflators in C</create>. Defaults to C<1>.

=head2 inflators

If true, processes inflators in C</default_values>. Defaults to C<1>.

=head1 SEE ALSO

L<HTML::FormFu>, L<Hash::Flatten>

=head1 AUTHOR

Moritz Onken, C<< onken@houseofdesign.de >>