Bug 20990: Add Koha::Account->outstanding_credits
[koha.git] / Koha / Object.pm
1 package Koha::Object;
2
3 # Copyright ByWater Solutions 2014
4 # Copyright 2016 Koha Development Team
5 #
6 # This file is part of Koha.
7 #
8 # Koha is free software; you can redistribute it and/or modify it under the
9 # terms of the GNU General Public License as published by the Free Software
10 # Foundation; either version 3 of the License, or (at your option) any later
11 # version.
12 #
13 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
14 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License along
18 # with Koha; if not, write to the Free Software Foundation, Inc.,
19 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
21 use Modern::Perl;
22
23 use Carp;
24 use Mojo::JSON;
25 use Scalar::Util qw( looks_like_number );
26 use Try::Tiny;
27
28 use Koha::Database;
29 use Koha::Exceptions::Object;
30 use Koha::DateUtils;
31
32 =head1 NAME
33
34 Koha::Object - Koha Object base class
35
36 =head1 SYNOPSIS
37
38     use Koha::Object;
39     my $object = Koha::Object->new({ property1 => $property1, property2 => $property2, etc... } );
40
41 =head1 DESCRIPTION
42
43 This class must always be subclassed.
44
45 =head1 API
46
47 =head2 Class Methods
48
49 =cut
50
51 =head3 Koha::Object->new();
52
53 my $object = Koha::Object->new();
54 my $object = Koha::Object->new($attributes);
55
56 Note that this cannot be used to retrieve record from the DB.
57
58 =cut
59
60 sub new {
61     my ( $class, $attributes ) = @_;
62     my $self = {};
63
64     if ($attributes) {
65         my $schema = Koha::Database->new->schema;
66
67         # Remove the arguments which exist, are not defined but NOT NULL to use the default value
68         my $columns_info = $schema->resultset( $class->_type )->result_source->columns_info;
69         for my $column_name ( keys %$attributes ) {
70             my $c_info = $columns_info->{$column_name};
71             next if $c_info->{is_nullable};
72             next if not exists $attributes->{$column_name} or defined $attributes->{$column_name};
73             delete $attributes->{$column_name};
74         }
75         $self->{_result} = $schema->resultset( $class->_type() )
76           ->new($attributes);
77     }
78
79     croak("No _type found! Koha::Object must be subclassed!")
80       unless $class->_type();
81
82     bless( $self, $class );
83
84 }
85
86 =head3 Koha::Object->_new_from_dbic();
87
88 my $object = Koha::Object->_new_from_dbic($dbic_row);
89
90 =cut
91
92 sub _new_from_dbic {
93     my ( $class, $dbic_row ) = @_;
94     my $self = {};
95
96     # DBIC result row
97     $self->{_result} = $dbic_row;
98
99     croak("No _type found! Koha::Object must be subclassed!")
100       unless $class->_type();
101
102     croak( "DBIC result _type " . ref( $self->{_result} ) . " isn't of the _type " . $class->_type() )
103       unless ref( $self->{_result} ) eq "Koha::Schema::Result::" . $class->_type();
104
105     bless( $self, $class );
106
107 }
108
109 =head3 $object->store();
110
111 Saves the object in storage.
112 If the object is new, it will be created.
113 If the object previously existed, it will be updated.
114
115 Returns:
116     $self  if the store was a success
117     undef  if the store failed
118
119 =cut
120
121 sub store {
122     my ($self) = @_;
123
124     try {
125         return $self->_result()->update_or_insert() ? $self : undef;
126     }
127     catch {
128         # Catch problems and raise relevant exceptions
129         if (ref($_) eq 'DBIx::Class::Exception') {
130             if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
131                 # FK constraints
132                 # FIXME: MySQL error, if we support more DB engines we should implement this for each
133                 if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
134                     Koha::Exceptions::Object::FKConstraint->throw(
135                         error     => 'Broken FK constraint',
136                         broken_fk => $+{column}
137                     );
138                 }
139             }
140             elsif( $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ ) {
141                 Koha::Exceptions::Object::DuplicateID->throw(
142                     error => 'Duplicate ID',
143                     duplicate_id => $+{key}
144                 );
145             }
146         }
147         # Catch-all for foreign key breakages. It will help find other use cases
148         $_->rethrow();
149     }
150 }
151
152 =head3 $object->delete();
153
154 Removes the object from storage.
155
156 Returns:
157     1  if the deletion was a success
158     0  if the deletion failed
159     -1 if the object was never in storage
160
161 =cut
162
163 sub delete {
164     my ($self) = @_;
165
166     # Deleting something not in storage throws an exception
167     return -1 unless $self->_result()->in_storage();
168
169     # Return a boolean for succcess
170     return $self->_result()->delete() ? 1 : 0;
171 }
172
173 =head3 $object->set( $properties_hashref )
174
175 $object->set(
176     {
177         property1 => $property1,
178         property2 => $property2,
179         property3 => $propery3,
180     }
181 );
182
183 Enables multiple properties to be set at once
184
185 Returns:
186     1      if all properties were set.
187     0      if one or more properties do not exist.
188     undef  if all properties exist but a different error
189            prevents one or more properties from being set.
190
191 If one or more of the properties do not exist,
192 no properties will be set.
193
194 =cut
195
196 sub set {
197     my ( $self, $properties ) = @_;
198
199     my @columns = @{$self->_columns()};
200
201     foreach my $p ( keys %$properties ) {
202         unless ( grep {/^$p$/} @columns ) {
203             Koha::Exceptions::Object::PropertyNotFound->throw( "No property $p for " . ref($self) );
204         }
205     }
206
207     return $self->_result()->set_columns($properties) ? $self : undef;
208 }
209
210 =head3 $object->unblessed();
211
212 Returns an unblessed representation of object.
213
214 =cut
215
216 sub unblessed {
217     my ($self) = @_;
218
219     return { $self->_result->get_columns };
220 }
221
222 =head3 $object->TO_JSON
223
224 Returns an unblessed representation of the object, suitable for JSON output.
225
226 =cut
227
228 sub TO_JSON {
229
230     my ($self) = @_;
231
232     my $unblessed    = $self->unblessed;
233     my $columns_info = Koha::Database->new->schema->resultset( $self->_type )
234         ->result_source->{_columns};
235
236     foreach my $col ( keys %{$columns_info} ) {
237
238         if ( $columns_info->{$col}->{is_boolean} )
239         {    # Handle booleans gracefully
240             $unblessed->{$col}
241                 = ( $unblessed->{$col} )
242                 ? Mojo::JSON->true
243                 : Mojo::JSON->false;
244         }
245         elsif ( _numeric_column_type( $columns_info->{$col}->{data_type} )
246             and looks_like_number( $unblessed->{$col} )
247         ) {
248
249             # TODO: Remove once the solution for
250             # https://rt.cpan.org/Ticket/Display.html?id=119904
251             # is ported to whatever distro we support by that time
252             $unblessed->{$col} += 0;
253         }
254         elsif ( _datetime_column_type( $columns_info->{$col}->{data_type} ) ) {
255             eval {
256                 return unless $unblessed->{$col};
257                 $unblessed->{$col} = output_pref({
258                     dateformat => 'rfc3339',
259                     dt         => dt_from_string($unblessed->{$col}, 'sql'),
260                 });
261             };
262         }
263     }
264     return $unblessed;
265 }
266
267 sub _datetime_column_type {
268     my ($column_type) = @_;
269
270     my @dt_types = (
271         'timestamp',
272         'datetime'
273     );
274
275     return ( grep { $column_type eq $_ } @dt_types) ? 1 : 0;
276 }
277
278 sub _numeric_column_type {
279     # TODO: Remove once the solution for
280     # https://rt.cpan.org/Ticket/Display.html?id=119904
281     # is ported to whatever distro we support by that time
282     my ($column_type) = @_;
283
284     my @numeric_types = (
285         'bigint',
286         'integer',
287         'int',
288         'mediumint',
289         'smallint',
290         'tinyint',
291         'decimal',
292         'double precision',
293         'float'
294     );
295
296     return ( grep { $column_type eq $_ } @numeric_types) ? 1 : 0;
297 }
298
299 =head3 $object->unblessed_all_relateds
300
301 my $everything_into_one_hashref = $object->unblessed_all_relateds
302
303 The unblessed method only retrieves column' values for the column of the object.
304 In a *few* cases we want to retrieve the information of all the prefetched data.
305
306 =cut
307
308 sub unblessed_all_relateds {
309     my ($self) = @_;
310
311     my %data;
312     my $related_resultsets = $self->_result->{related_resultsets} || {};
313     my $rs = $self;
314     while ( $related_resultsets and %$related_resultsets ) {
315         my @relations = keys %{ $related_resultsets };
316         if ( @relations ) {
317             my $relation = $relations[0];
318             $rs = $rs->related_resultset($relation)->get_cache;
319             $rs = $rs->[0]; # Does it makes sense to have several values here?
320             my $object_class = Koha::Object::_get_object_class( $rs->result_class );
321             my $koha_object = $object_class->_new_from_dbic( $rs );
322             $related_resultsets = $rs->{related_resultsets};
323             %data = ( %data, %{ $koha_object->unblessed } );
324         }
325     }
326     %data = ( %data, %{ $self->unblessed } );
327     return \%data;
328 }
329
330 =head3 $object->_result();
331
332 Returns the internal DBIC Row object
333
334 =cut
335
336 sub _result {
337     my ($self) = @_;
338
339     # If we don't have a dbic row at this point, we need to create an empty one
340     $self->{_result} ||=
341       Koha::Database->new()->schema()->resultset( $self->_type() )->new({});
342
343     return $self->{_result};
344 }
345
346 =head3 $object->_columns();
347
348 Returns an arrayref of the table columns
349
350 =cut
351
352 sub _columns {
353     my ($self) = @_;
354
355     # If we don't have a dbic row at this point, we need to create an empty one
356     $self->{_columns} ||= [ $self->_result()->result_source()->columns() ];
357
358     return $self->{_columns};
359 }
360
361 sub _get_object_class {
362     my ( $type ) = @_;
363     return unless $type;
364
365     if( $type->can('koha_object_class') ) {
366         return $type->koha_object_class;
367     }
368     $type =~ s|Schema::Result::||;
369     return ${type};
370 }
371
372 =head3 AUTOLOAD
373
374 The autoload method is used only to get and set values for an objects properties.
375
376 =cut
377
378 sub AUTOLOAD {
379     my $self = shift;
380
381     my $method = our $AUTOLOAD;
382     $method =~ s/.*://;
383
384     my @columns = @{$self->_columns()};
385     # Using direct setter/getter like $item->barcode() or $item->barcode($barcode);
386     if ( grep {/^$method$/} @columns ) {
387         if ( @_ ) {
388             $self->_result()->set_column( $method, @_ );
389             return $self;
390         } else {
391             my $value = $self->_result()->get_column( $method );
392             return $value;
393         }
394     }
395
396     my @known_methods = qw( is_changed id in_storage get_column discard_changes update related_resultset make_column_dirty );
397
398     Koha::Exceptions::Object::MethodNotCoveredByTests->throw(
399         error      => sprintf("The method %s->%s is not covered by tests!", ref($self), $method),
400         show_trace => 1
401     ) unless grep { /^$method$/ } @known_methods;
402
403
404     my $r = eval { $self->_result->$method(@_) };
405     if ( $@ ) {
406         Koha::Exceptions::Object->throw( ref($self) . "::$method generated this error: " . $@ );
407     }
408     return $r;
409 }
410
411 =head3 _type
412
413 This method must be defined in the child class. The value is the name of the DBIC resultset.
414 For example, for borrowers, the _type method will return "Borrower".
415
416 =cut
417
418 sub _type { }
419
420 sub DESTROY { }
421
422 =head1 AUTHOR
423
424 Kyle M Hall <kyle@bywatersolutions.com>
425
426 Jonathan Druart <jonathan.druart@bugs.koha-community.org>
427
428 =cut
429
430 1;