Bug 21663: DBRev 19.05.003
[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     my $columns_info = $self->_result->result_source->columns_info;
125
126     # Handle not null and default values for integers and dates
127     foreach my $col ( keys %{$columns_info} ) {
128         # Integers
129         if ( _numeric_column_type( $columns_info->{$col}->{data_type} ) ) {
130             # Has been passed but not a number, usually an empty string
131             if ( defined $self->$col and not looks_like_number( $self->$col ) ) {
132                 if ( $columns_info->{$col}->{is_nullable} ) {
133                     # If nullable, default to null
134                     $self->$col(undef);
135                 } else {
136                     # If cannot be null, get the default value
137                     # What if cannot be null and does not have a default value? Possible?
138                     $self->$col($columns_info->{$col}->{default_value});
139                 }
140             }
141         }
142         elsif ( _date_or_datetime_column_type( $columns_info->{$col}->{data_type} ) ) {
143             # Set to null if an empty string (or == 0 but should not happen)
144             if ( defined $self->$col and not $self->$col ) {
145                 if ( $columns_info->{$col}->{is_nullable} ) {
146                     $self->$col(undef);
147                 } else {
148                     $self->$col($columns_info->{$col}->{default_value});
149                 }
150             }
151         }
152     }
153
154     try {
155         return $self->_result()->update_or_insert() ? $self : undef;
156     }
157     catch {
158         # Catch problems and raise relevant exceptions
159         if (ref($_) eq 'DBIx::Class::Exception') {
160             if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
161                 # FK constraints
162                 # FIXME: MySQL error, if we support more DB engines we should implement this for each
163                 if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
164                     Koha::Exceptions::Object::FKConstraint->throw(
165                         error     => 'Broken FK constraint',
166                         broken_fk => $+{column}
167                     );
168                 }
169             }
170             elsif( $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ ) {
171                 Koha::Exceptions::Object::DuplicateID->throw(
172                     error => 'Duplicate ID',
173                     duplicate_id => $+{key}
174                 );
175             }
176             elsif( $_->{msg} =~ /Incorrect (?<type>\w+) value: '(?<value>.*)' for column '(?<property>\w+)'/ ) {
177                 Koha::Exceptions::Object::BadValue->throw(
178                     type     => $+{type},
179                     value    => $+{value},
180                     property => $+{property}
181                 );
182             }
183         }
184         # Catch-all for foreign key breakages. It will help find other use cases
185         $_->rethrow();
186     }
187 }
188
189 =head3 $object->delete();
190
191 Removes the object from storage.
192
193 Returns:
194     1  if the deletion was a success
195     0  if the deletion failed
196     -1 if the object was never in storage
197
198 =cut
199
200 sub delete {
201     my ($self) = @_;
202
203     # Deleting something not in storage throws an exception
204     return -1 unless $self->_result()->in_storage();
205
206     # Return a boolean for succcess
207     return $self->_result()->delete() ? 1 : 0;
208 }
209
210 =head3 $object->set( $properties_hashref )
211
212 $object->set(
213     {
214         property1 => $property1,
215         property2 => $property2,
216         property3 => $propery3,
217     }
218 );
219
220 Enables multiple properties to be set at once
221
222 Returns:
223     1      if all properties were set.
224     0      if one or more properties do not exist.
225     undef  if all properties exist but a different error
226            prevents one or more properties from being set.
227
228 If one or more of the properties do not exist,
229 no properties will be set.
230
231 =cut
232
233 sub set {
234     my ( $self, $properties ) = @_;
235
236     my @columns = @{$self->_columns()};
237
238     foreach my $p ( keys %$properties ) {
239         unless ( grep {/^$p$/} @columns ) {
240             Koha::Exceptions::Object::PropertyNotFound->throw( "No property $p for " . ref($self) );
241         }
242     }
243
244     return $self->_result()->set_columns($properties) ? $self : undef;
245 }
246
247 =head3 $object->unblessed();
248
249 Returns an unblessed representation of object.
250
251 =cut
252
253 sub unblessed {
254     my ($self) = @_;
255
256     return { $self->_result->get_columns };
257 }
258
259 =head3 $object->get_from_storage;
260
261 =cut
262
263 sub get_from_storage {
264     my ( $self, $attrs ) = @_;
265     my $stored_object = $self->_result->get_from_storage($attrs);
266     return unless $stored_object;
267     my $object_class  = Koha::Object::_get_object_class( $self->_result->result_class );
268     return $object_class->_new_from_dbic($stored_object);
269 }
270
271 =head3 $object->TO_JSON
272
273 Returns an unblessed representation of the object, suitable for JSON output.
274
275 =cut
276
277 sub TO_JSON {
278
279     my ($self) = @_;
280
281     my $unblessed    = $self->unblessed;
282     my $columns_info = Koha::Database->new->schema->resultset( $self->_type )
283         ->result_source->{_columns};
284
285     foreach my $col ( keys %{$columns_info} ) {
286
287         if ( $columns_info->{$col}->{is_boolean} )
288         {    # Handle booleans gracefully
289             $unblessed->{$col}
290                 = ( $unblessed->{$col} )
291                 ? Mojo::JSON->true
292                 : Mojo::JSON->false;
293         }
294         elsif ( _numeric_column_type( $columns_info->{$col}->{data_type} )
295             and looks_like_number( $unblessed->{$col} )
296         ) {
297
298             # TODO: Remove once the solution for
299             # https://rt.cpan.org/Ticket/Display.html?id=119904
300             # is ported to whatever distro we support by that time
301             $unblessed->{$col} += 0;
302         }
303         elsif ( _datetime_column_type( $columns_info->{$col}->{data_type} ) ) {
304             eval {
305                 return unless $unblessed->{$col};
306                 $unblessed->{$col} = output_pref({
307                     dateformat => 'rfc3339',
308                     dt         => dt_from_string($unblessed->{$col}, 'sql'),
309                 });
310             };
311         }
312     }
313     return $unblessed;
314 }
315
316 sub _date_or_datetime_column_type {
317     my ($column_type) = @_;
318
319     my @dt_types = (
320         'timestamp',
321         'date',
322         'datetime'
323     );
324
325     return ( grep { $column_type eq $_ } @dt_types) ? 1 : 0;
326 }
327 sub _datetime_column_type {
328     my ($column_type) = @_;
329
330     my @dt_types = (
331         'timestamp',
332         'datetime'
333     );
334
335     return ( grep { $column_type eq $_ } @dt_types) ? 1 : 0;
336 }
337
338 sub _numeric_column_type {
339     # TODO: Remove once the solution for
340     # https://rt.cpan.org/Ticket/Display.html?id=119904
341     # is ported to whatever distro we support by that time
342     my ($column_type) = @_;
343
344     my @numeric_types = (
345         'bigint',
346         'integer',
347         'int',
348         'mediumint',
349         'smallint',
350         'tinyint',
351         'decimal',
352         'double precision',
353         'float'
354     );
355
356     return ( grep { $column_type eq $_ } @numeric_types) ? 1 : 0;
357 }
358
359 =head3 $object->unblessed_all_relateds
360
361 my $everything_into_one_hashref = $object->unblessed_all_relateds
362
363 The unblessed method only retrieves column' values for the column of the object.
364 In a *few* cases we want to retrieve the information of all the prefetched data.
365
366 =cut
367
368 sub unblessed_all_relateds {
369     my ($self) = @_;
370
371     my %data;
372     my $related_resultsets = $self->_result->{related_resultsets} || {};
373     my $rs = $self->_result;
374     while ( $related_resultsets and %$related_resultsets ) {
375         my @relations = keys %{ $related_resultsets };
376         if ( @relations ) {
377             my $relation = $relations[0];
378             $rs = $rs->related_resultset($relation)->get_cache;
379             $rs = $rs->[0]; # Does it makes sense to have several values here?
380             my $object_class = Koha::Object::_get_object_class( $rs->result_class );
381             my $koha_object = $object_class->_new_from_dbic( $rs );
382             $related_resultsets = $rs->{related_resultsets};
383             %data = ( %data, %{ $koha_object->unblessed } );
384         }
385     }
386     %data = ( %data, %{ $self->unblessed } );
387     return \%data;
388 }
389
390 =head3 $object->_result();
391
392 Returns the internal DBIC Row object
393
394 =cut
395
396 sub _result {
397     my ($self) = @_;
398
399     # If we don't have a dbic row at this point, we need to create an empty one
400     $self->{_result} ||=
401       Koha::Database->new()->schema()->resultset( $self->_type() )->new({});
402
403     return $self->{_result};
404 }
405
406 =head3 $object->_columns();
407
408 Returns an arrayref of the table columns
409
410 =cut
411
412 sub _columns {
413     my ($self) = @_;
414
415     # If we don't have a dbic row at this point, we need to create an empty one
416     $self->{_columns} ||= [ $self->_result()->result_source()->columns() ];
417
418     return $self->{_columns};
419 }
420
421 sub _get_object_class {
422     my ( $type ) = @_;
423     return unless $type;
424
425     if( $type->can('koha_object_class') ) {
426         return $type->koha_object_class;
427     }
428     $type =~ s|Schema::Result::||;
429     return ${type};
430 }
431
432 =head3 AUTOLOAD
433
434 The autoload method is used only to get and set values for an objects properties.
435
436 =cut
437
438 sub AUTOLOAD {
439     my $self = shift;
440
441     my $method = our $AUTOLOAD;
442     $method =~ s/.*://;
443
444     my @columns = @{$self->_columns()};
445     # Using direct setter/getter like $item->barcode() or $item->barcode($barcode);
446     if ( grep {/^$method$/} @columns ) {
447         if ( @_ ) {
448             $self->_result()->set_column( $method, @_ );
449             return $self;
450         } else {
451             my $value = $self->_result()->get_column( $method );
452             return $value;
453         }
454     }
455
456     my @known_methods = qw( is_changed id in_storage get_column discard_changes update make_column_dirty );
457
458     Koha::Exceptions::Object::MethodNotCoveredByTests->throw(
459         error      => sprintf("The method %s->%s is not covered by tests!", ref($self), $method),
460         show_trace => 1
461     ) unless grep { /^$method$/ } @known_methods;
462
463
464     my $r = eval { $self->_result->$method(@_) };
465     if ( $@ ) {
466         Koha::Exceptions::Object->throw( ref($self) . "::$method generated this error: " . $@ );
467     }
468     return $r;
469 }
470
471 =head3 _type
472
473 This method must be defined in the child class. The value is the name of the DBIC resultset.
474 For example, for borrowers, the _type method will return "Borrower".
475
476 =cut
477
478 sub _type { }
479
480 sub DESTROY { }
481
482 =head1 AUTHOR
483
484 Kyle M Hall <kyle@bywatersolutions.com>
485
486 Jonathan Druart <jonathan.druart@bugs.koha-community.org>
487
488 =cut
489
490 1;