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