FilterColumn tweaks - docs and parameter tests
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / FilterColumn.pm
CommitLineData
51bec050 1package DBIx::Class::FilterColumn;
51bec050 2use strict;
3use warnings;
4
5use base qw/DBIx::Class::Row/;
6
7sub filter_column {
8 my ($self, $col, $attrs) = @_;
9
eec182b6 10 $self->throw_exception('FilterColumn does not work with InflateColumn')
c227b295 11 if $self->isa('DBIx::Class::InflateColumn') &&
12 defined $self->column_info($col)->{_inflate_info};
13
51bec050 14 $self->throw_exception("No such column $col to filter")
15 unless $self->has_column($col);
16
eec182b6 17 $self->throw_exception('filter_column expects a hashref of filter specifications')
51bec050 18 unless ref $attrs eq 'HASH';
19
eec182b6 20 $self->throw_exception('An invocation of filter_column() must specify either a filter_from_storage or filter_to_storage')
21 unless $attrs->{filter_from_storage} || $attrs->{filter_to_storage};
22
51bec050 23 $self->column_info($col)->{_filter_info} = $attrs;
24 my $acc = $self->column_info($col)->{accessor};
d7d38bef 25 $self->mk_group_accessors(filtered_column => [ (defined $acc ? $acc : $col), $col]);
51bec050 26 return 1;
27}
28
4c0c3038 29sub _column_from_storage {
9b2c0de6 30 my ($self, $col, $value) = @_;
51bec050 31
32 return $value unless defined $value;
33
34 my $info = $self->column_info($col)
35 or $self->throw_exception("No column info for $col");
36
37 return $value unless exists $info->{_filter_info};
38
d7d38bef 39 my $filter = $info->{_filter_info}{filter_from_storage};
51bec050 40
eec182b6 41 return defined $filter ? $self->$filter($value) : $value;
51bec050 42}
43
4c0c3038 44sub _column_to_storage {
9b2c0de6 45 my ($self, $col, $value) = @_;
51bec050 46
47 my $info = $self->column_info($col) or
48 $self->throw_exception("No column info for $col");
49
50 return $value unless exists $info->{_filter_info};
51
d7d38bef 52 my $unfilter = $info->{_filter_info}{filter_to_storage};
eec182b6 53
54 return defined $unfilter ? $self->$unfilter($value) : $value;
51bec050 55}
56
d7d38bef 57sub get_filtered_column {
51bec050 58 my ($self, $col) = @_;
59
60 $self->throw_exception("$col is not a filtered column")
61 unless exists $self->column_info($col)->{_filter_info};
62
63 return $self->{_filtered_column}{$col}
64 if exists $self->{_filtered_column}{$col};
65
66 my $val = $self->get_column($col);
67
9b2c0de6 68 return $self->{_filtered_column}{$col} = $self->_column_from_storage($col, $val);
51bec050 69}
70
491c8ff9 71sub get_column {
72 my ($self, $col) = @_;
73 if (exists $self->{_filtered_column}{$col}) {
74 return $self->{_column_data}{$col} ||= $self->_column_to_storage ($col, $self->{_filtered_column}{$col});
75 }
76
77 return $self->next::method ($col);
78}
79
80# sadly a separate codepath in Row.pm ( used by insert() )
81sub get_columns {
82 my $self = shift;
83
84 foreach my $col (keys %{$self->{_filtered_column}||{}}) {
85 $self->{_column_data}{$col} ||= $self->_column_to_storage ($col, $self->{_filtered_column}{$col})
86 if exists $self->{_filtered_column}{$col};
87 }
88
89 $self->next::method (@_);
90}
91
92sub store_column {
85439e0c 93 my ($self, $col) = (shift, @_);
94
95 # blow cache
96 delete $self->{_filtered_column}{$col};
97
98 $self->next::method(@_);
99}
100
d7d38bef 101sub set_filtered_column {
51bec050 102 my ($self, $col, $filtered) = @_;
103
cde96798 104 # do not blow up the cache via set_column unless necessary
105 # (filtering may be expensive!)
106 if (exists $self->{_filtered_column}{$col}) {
107 return $filtered
108 if ($self->_eq_column_values ($col, $filtered, $self->{_filtered_column}{$col} ) );
51bec050 109
cde96798 110 $self->make_column_dirty ($col); # so the comparison won't run again
111 }
112
113 $self->set_column($col, $self->_column_to_storage($col, $filtered));
51bec050 114
491c8ff9 115 return $self->{_filtered_column}{$col} = $filtered;
51bec050 116}
117
7b461f8a 118sub update {
119 my ($self, $attrs, @rest) = @_;
491c8ff9 120
7b461f8a 121 foreach my $key (keys %{$attrs||{}}) {
491c8ff9 122 if (
123 $self->has_column($key)
124 &&
125 exists $self->column_info($key)->{_filter_info}
126 ) {
127 $self->set_filtered_column($key, delete $attrs->{$key});
7c6fa77f 128
129 # FIXME update() reaches directly into the object-hash
130 # and we may *not* have a filtered value there - thus
131 # the void-ctx filter-trigger
132 $self->get_column($key) unless exists $self->{_column_data}{$key};
7b461f8a 133 }
134 }
491c8ff9 135
7b461f8a 136 return $self->next::method($attrs, @rest);
137}
138
7b461f8a 139sub new {
140 my ($class, $attrs, @rest) = @_;
9b2c0de6 141 my $source = $attrs->{-result_source}
d9ea6d6d 142 or $class->throw_exception('Sourceless rows are not supported with DBIx::Class::FilterColumn');
143
9b2c0de6 144 my $obj = $class->next::method($attrs, @rest);
7b461f8a 145 foreach my $key (keys %{$attrs||{}}) {
9b2c0de6 146 if ($obj->has_column($key) &&
147 exists $obj->column_info($key)->{_filter_info} ) {
491c8ff9 148 $obj->set_filtered_column($key, $attrs->{$key});
7b461f8a 149 }
150 }
491c8ff9 151
7b461f8a 152 return $obj;
153}
154
51bec050 1551;
22d9e05a 156
9b2c0de6 157=head1 NAME
22d9e05a 158
9b2c0de6 159DBIx::Class::FilterColumn - Automatically convert column data
160
161=head1 SYNOPSIS
162
f10a2e9a 163In your Schema or DB class add "FilterColumn" to the top of the component list.
164
165 __PACKAGE__->load_components(qw( FilterColumn ... ));
166
167Set up filters for the columns you want to convert.
168
9b2c0de6 169 __PACKAGE__->filter_column( money => {
170 filter_to_storage => 'to_pennies',
171 filter_from_storage => 'from_pennies',
172 });
22d9e05a 173
174 sub to_pennies { $_[1] * 100 }
9b2c0de6 175
22d9e05a 176 sub from_pennies { $_[1] / 100 }
177
178 1;
179
f10a2e9a 180
9b2c0de6 181=head1 DESCRIPTION
22d9e05a 182
9b2c0de6 183This component is meant to be a more powerful, but less DWIM-y,
184L<DBIx::Class::InflateColumn>. One of the major issues with said component is
185that it B<only> works with references. Generally speaking anything that can
186be done with L<DBIx::Class::InflateColumn> can be done with this component.
22d9e05a 187
9b2c0de6 188=head1 METHODS
22d9e05a 189
9b2c0de6 190=head2 filter_column
22d9e05a 191
9b2c0de6 192 __PACKAGE__->filter_column( colname => {
eec182b6 193 filter_from_storage => 'method'|\&coderef,
194 filter_to_storage => 'method'|\&coderef,
9b2c0de6 195 })
22d9e05a 196
eec182b6 197This is the method that you need to call to set up a filtered column. It takes
198exactly two arguments; the first being the column name the second being a hash
199reference with C<filter_from_storage> and C<filter_to_storage> set to either
200a method name or a code reference. In either case the filter is invoked as:
201
202 $row_obj->$filter_specification ($value_to_filter)
203
204with C<$filter_specification> being chosen depending on whether the
205C<$value_to_filter> is being retrieved from or written to permanent
206storage.
207
208If a specific directional filter is not specified, the original value will be
209passed to/from storage unfiltered.
22d9e05a 210
9b2c0de6 211=head2 get_filtered_column
22d9e05a 212
9b2c0de6 213 $obj->get_filtered_column('colname')
214
215Returns the filtered value of the column
216
217=head2 set_filtered_column
218
219 $obj->set_filtered_column(colname => 'new_value')
220
221Sets the filtered value of the column
eec182b6 222
223=head1 EXAMPLE OF USE
224
225Some databases have restrictions on values that can be passed to
226boolean columns, and problems can be caused by passing value that
227perl considers to be false (such as C<undef>).
228
229One solution to this is to ensure that the boolean values are set
230to something that the database can handle - such as numeric zero
231and one, using code like this:-
232
233 __PACKAGE__->filter_column(
234 my_boolean_column => {
235 filter_to_storage => sub { $_[1] ? 1 : 0 },
236 }
237 );
238
239In this case the C<filter_from_storage> is not required, as just
240passing the database value through to perl does the right thing.