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