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