3 # Copyright ByWater Solutions 2014
4 # Copyright 2016 Koha Development Team
6 # This file is part of Koha.
8 # Koha is free software; you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
13 # Koha is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with Koha; if not, see <http://www.gnu.org/licenses>.
25 use Scalar::Util qw( blessed looks_like_number );
26 use Try::Tiny qw( catch try );
27 use List::MoreUtils qw( any );
28 use DateTime::Format::MySQL;
31 use Koha::DateTime::Format::RFC3339;
32 use Koha::DateTime::Format::SQL;
33 use Koha::Exceptions::Object;
34 use Koha::Object::Message;
38 Koha::Object - Koha Object base class
43 my $object = Koha::Object->new({ property1 => $property1, property2 => $property2, etc... } );
47 This class must always be subclassed.
55 =head3 Koha::Object->new();
57 my $object = Koha::Object->new();
58 my $object = Koha::Object->new($attributes);
60 Note that this cannot be used to retrieve record from the DB.
65 my ( $class, $attributes ) = @_;
69 my $schema = Koha::Database->new->schema;
71 # Remove the arguments which exist, are not defined but NOT NULL to use the default value
72 my $columns_info = $schema->resultset( $class->_type )->result_source->columns_info;
73 for my $column_name ( keys %$attributes ) {
74 my $c_info = $columns_info->{$column_name};
75 next if $c_info->{is_nullable};
76 next if not exists $attributes->{$column_name} or defined $attributes->{$column_name};
77 delete $attributes->{$column_name};
81 $schema->resultset( $class->_type() )->new($attributes);
84 $self->{_messages} = [];
86 croak("No _type found! Koha::Object must be subclassed!")
87 unless $class->_type();
89 bless( $self, $class );
93 =head3 Koha::Object->_new_from_dbic();
95 my $object = Koha::Object->_new_from_dbic($dbic_row);
100 my ( $class, $dbic_row ) = @_;
104 $self->{_result} = $dbic_row;
106 croak("No _type found! Koha::Object must be subclassed!")
107 unless $class->_type();
109 croak( "DBIC result _type " . ref( $self->{_result} ) . " isn't of the _type " . $class->_type() )
110 unless ref( $self->{_result} ) eq "Koha::Schema::Result::" . $class->_type();
112 bless( $self, $class );
116 =head3 $object->store();
118 Saves the object in storage.
119 If the object is new, it will be created.
120 If the object previously existed, it will be updated.
123 $self if the store was a success
124 undef if the store failed
131 my $columns_info = $self->_result->result_source->columns_info;
133 # Handle not null and default values for integers and dates
134 foreach my $col ( keys %{$columns_info} ) {
136 if ( _numeric_column_type( $columns_info->{$col}->{data_type} )
137 or _decimal_column_type( $columns_info->{$col}->{data_type} )
139 # Has been passed but not a number, usually an empty string
140 my $value = $self->_result()->get_column($col);
141 if ( defined $value and not looks_like_number( $value ) ) {
142 if ( $columns_info->{$col}->{is_nullable} ) {
143 # If nullable, default to null
144 $self->_result()->set_column($col => undef);
146 # If cannot be null, get the default value
147 # What if cannot be null and does not have a default value? Possible?
148 $self->_result()->set_column($col => $columns_info->{$col}->{default_value});
152 elsif ( _date_or_datetime_column_type( $columns_info->{$col}->{data_type} ) ) {
153 # Set to null if an empty string (or == 0 but should not happen)
154 my $value = $self->_result()->get_column($col);
155 if ( defined $value and not $value ) {
156 if ( $columns_info->{$col}->{is_nullable} ) {
157 $self->_result()->set_column($col => undef);
159 $self->_result()->set_column($col => $columns_info->{$col}->{default_value});
162 elsif ( not defined $self->$col
163 && $columns_info->{$col}->{datetime_undef_if_invalid} )
166 $self->_result()->set_column($col => $columns_info->{$col}->{default_value});
172 return $self->_result()->update_or_insert() ? $self : undef;
175 # Catch problems and raise relevant exceptions
176 if (ref($_) eq 'DBIx::Class::Exception') {
178 if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
180 # FIXME: MySQL error, if we support more DB engines we should implement this for each
181 if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
182 Koha::Exceptions::Object::FKConstraint->throw(
183 error => 'Broken FK constraint',
184 broken_fk => $+{column}
188 elsif( $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ ) {
189 Koha::Exceptions::Object::DuplicateID->throw(
190 error => 'Duplicate ID',
191 duplicate_id => $+{key}
194 elsif( $_->{msg} =~ /Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/ ) { # The optional \W in the regex might be a quote or backtick
196 my $value = $+{value};
197 my $property = $+{property};
198 $property =~ s/['`]//g;
199 Koha::Exceptions::Object::BadValue->throw(
202 property => $property =~ /(\w+\.\w+)$/ ? $1 : $property, # results in table.column without quotes or backtics
206 # Catch-all for foreign key breakages. It will help find other use cases
211 =head3 $object->update();
213 A shortcut for set + store in one call.
218 my ($self, $values) = @_;
219 Koha::Exceptions::Object::NotInStorage->throw unless $self->in_storage;
220 $self->set($values)->store();
223 =head3 $object->delete();
225 Removes the object from storage.
228 The item object if deletion was a success
229 The DBIX::Class error if deletion failed
236 my $deleted = $self->_result()->delete;
237 if ( ref $deleted ) {
238 my $object_class = Koha::Object::_get_object_class( $self->_result->result_class );
239 $deleted = $object_class->_new_from_dbic($deleted);
244 =head3 $object->set( $properties_hashref )
248 property1 => $property1,
249 property2 => $property2,
250 property3 => $propery3,
254 Enables multiple properties to be set at once
257 1 if all properties were set.
258 0 if one or more properties do not exist.
259 undef if all properties exist but a different error
260 prevents one or more properties from being set.
262 If one or more of the properties do not exist,
263 no properties will be set.
268 my ( $self, $properties ) = @_;
270 my @columns = @{$self->_columns()};
272 foreach my $p ( keys %$properties ) {
273 unless ( grep { $_ eq $p } @columns ) {
274 Koha::Exceptions::Object::PropertyNotFound->throw( "No property $p for " . ref($self) );
278 return $self->_result()->set_columns($properties) ? $self : undef;
281 =head3 $object->set_or_blank( $properties_hashref )
283 $object->set_or_blank(
285 property1 => $property1,
286 property2 => $property2,
287 property3 => $propery3,
291 If not listed in $properties_hashref, the property will be set to the default
292 value defined at DB level, or nulled.
298 my ( $self, $properties ) = @_;
300 my $columns_info = $self->_result->result_source->columns_info;
302 foreach my $col ( keys %{$columns_info} ) {
304 next if exists $properties->{$col};
306 if ( $columns_info->{$col}->{is_nullable} ) {
307 $properties->{$col} = undef;
309 $properties->{$col} = $columns_info->{$col}->{default_value};
313 return $self->set($properties);
316 =head3 $object->unblessed();
318 Returns an unblessed representation of object.
325 return { $self->_result->get_columns };
328 =head3 $object->get_from_storage;
332 sub get_from_storage {
333 my ( $self, $attrs ) = @_;
334 my $stored_object = $self->_result->get_from_storage($attrs);
335 return unless $stored_object;
336 my $object_class = Koha::Object::_get_object_class( $self->_result->result_class );
337 return $object_class->_new_from_dbic($stored_object);
340 =head3 $object->object_messages
342 my @messages = @{ $object->object_messages };
344 Returns the (probably non-fatal) messages that were recorded on the object.
348 sub object_messages {
351 $self->{_messages} = []
352 unless defined $self->{_messages};
354 return $self->{_messages};
357 =head3 $object->add_message
360 <some action that might fail>
363 if ( <fatal condition> ) {
364 Koha::Exception->throw...
367 # This is a non fatal error, notify the caller
368 $self->add_message({ message => $error, type => 'error' });
377 my ( $self, $params ) = @_;
379 push @{ $self->{_messages} }, Koha::Object::Message->new($params);
384 =head3 $object->TO_JSON
386 Returns an unblessed representation of the object, suitable for JSON output.
394 my $unblessed = $self->unblessed;
395 my $columns_info = Koha::Database->new->schema->resultset( $self->_type )
396 ->result_source->{_columns};
398 foreach my $col ( keys %{$columns_info} ) {
400 if ( $columns_info->{$col}->{is_boolean} )
401 { # Handle booleans gracefully
403 = ( $unblessed->{$col} )
407 elsif ( _numeric_column_type( $columns_info->{$col}->{data_type} )
408 and looks_like_number( $unblessed->{$col} )
410 # TODO: Remove once the solution for
411 # https://github.com/perl5-dbi/DBD-mysql/issues/212
412 # is ported to whatever distro we support by that time
413 # or we move to DBD::MariaDB
414 $unblessed->{$col} += 0;
416 elsif ( _decimal_column_type( $columns_info->{$col}->{data_type} )
417 and looks_like_number( $unblessed->{$col} )
419 # TODO: Remove once the solution for
420 # https://github.com/perl5-dbi/DBD-mysql/issues/212
421 # is ported to whatever distro we support by that time
422 # or we move to DBD::MariaDB
423 $unblessed->{$col} += 0.00;
425 elsif ( _datetime_column_type( $columns_info->{$col}->{data_type} ) ) {
427 return unless $unblessed->{$col};
428 my $dt = Koha::DateTime::Format::SQL->parse_datetime( $unblessed->{$col} );
429 $unblessed->{$col} = Koha::DateTime::Format::RFC3339->format_datetime($dt);
436 sub _date_or_datetime_column_type {
437 my ($column_type) = @_;
445 return ( grep { $column_type eq $_ } @dt_types) ? 1 : 0;
447 sub _datetime_column_type {
448 my ($column_type) = @_;
455 return ( grep { $column_type eq $_ } @dt_types) ? 1 : 0;
458 sub _numeric_column_type {
459 # TODO: Remove once the solution for
460 # https://github.com/perl5-dbi/DBD-mysql/issues/212
461 # is ported to whatever distro we support by that time
462 # or we move to DBD::MariaDB
463 my ($column_type) = @_;
465 my @numeric_types = (
474 return ( grep { $column_type eq $_ } @numeric_types) ? 1 : 0;
477 sub _decimal_column_type {
478 # TODO: Remove once the solution for
479 # https://github.com/perl5-dbi/DBD-mysql/issues/212
480 # is ported to whatever distro we support by that time
481 # or we move to DBD::MariaDB
482 my ($column_type) = @_;
484 my @decimal_types = (
490 return ( grep { $column_type eq $_ } @decimal_types) ? 1 : 0;
493 =head3 prefetch_whitelist
495 my $whitelist = $object->prefetch_whitelist()
497 Returns a hash of prefetchable subs and the type they return.
501 sub prefetch_whitelist {
505 my $relations = $self->_result->result_source->_relationships;
507 foreach my $key (keys %{$relations}) {
508 if($self->can($key)) {
509 my $result_class = $relations->{$key}->{class};
510 my $obj = $result_class->new;
512 $whitelist->{$key} = Koha::Object::_get_object_class( $obj->result_class );
514 $whitelist->{$key} = undef;
524 my $object_for_api = $object->to_api(
546 Returns a representation of the object, suitable for API output.
551 my ( $self, $params ) = @_;
553 my $json_object = $self->TO_JSON;
555 # Make sure we duplicate the $params variable to avoid
556 # breaking calls in a loop (Koha::Objects->to_api)
557 $params = defined $params ? {%$params} : {};
559 # children should be able to handle without
560 my $embeds = delete $params->{embed};
561 my $strings = delete $params->{strings};
563 # coded values handling
565 if ( $strings and $self->can('strings_map') ) {
566 $string_map = $self->strings_map($params);
569 # Remove forbidden attributes if required (including their coded values)
570 if ( $params->{public} ) {
571 for my $field ( keys %{$json_object} ) {
572 delete $json_object->{$field}
573 unless any { $_ eq $field } @{ $self->public_read_list };
577 foreach my $field ( keys %{$string_map} ) {
578 delete $string_map->{$field}
579 unless any { $_ eq $field } @{ $self->public_read_list };
584 # Remove forbidden attributes if required (including their coded values)
585 if ( !$self->is_accessible($params) ) {
586 for my $field ( keys %{$json_object} ) {
587 unless ( any { $_ eq $field } @{ $self->unredact_list } ) {
588 $json_object->{$field} = undef;
593 foreach my $field ( keys %{$string_map} ) {
594 unless ( any { $_ eq $field } @{ $self->unredact_list } ) {
595 $string_map->{$field} = undef;
601 my $to_api_mapping = $self->to_api_mapping;
603 # Rename attributes and coded values if there's a mapping
604 if ( $self->can('to_api_mapping') ) {
605 foreach my $column ( keys %{ $self->to_api_mapping } ) {
606 my $mapped_column = $self->to_api_mapping->{$column};
607 if ( exists $json_object->{$column}
608 && defined $mapped_column )
612 $json_object->{$mapped_column} = delete $json_object->{$column};
613 $string_map->{$mapped_column} = delete $string_map->{$column}
614 if exists $string_map->{$column};
617 elsif ( exists $json_object->{$column}
618 && !defined $mapped_column )
622 delete $json_object->{$column};
623 delete $string_map->{$column};
628 $json_object->{_strings} = $string_map
632 foreach my $embed ( keys %{$embeds} ) {
633 if ( $embed =~ m/^(?<relation>.*)_count$/
634 and $embeds->{$embed}->{is_count} )
637 my $relation = $+{relation};
638 $json_object->{$embed} = $self->$relation->count;
642 my $next = $embeds->{$curr}->{children};
644 $params->{strings} = 1
645 if $embeds->{$embed}->{strings};
647 my $children = $self->$curr;
649 if ( defined $children and ref($children) eq 'ARRAY' ) {
651 $self->_handle_to_api_child(
660 $json_object->{$curr} = \@list;
663 $json_object->{$curr} = $self->_handle_to_api_child(
679 =head3 to_api_mapping
681 my $mapping = $object->to_api_mapping;
683 Generic method that returns the attribute name mappings required to
684 render the object on the API.
686 Note: this only returns an empty I<hashref>. Each class should have its
687 own mapping returned.
697 my $string_map = $object->strings_map($params);
699 Generic method that returns the string map for coded attributes.
701 Return should be a hashref keyed on database field name with the values
702 being hashrefs containing 'str', 'type' and optionally 'category'.
704 This is then used in to_api to render the _strings embed when requested.
706 Note: this only returns an empty I<hashref>. Each class should have its
707 own mapping returned.
715 =head3 public_read_list
718 my @public_read_list = @{$object->public_read_list};
720 Generic method that returns the list of database columns that are allowed to
721 be passed to render objects on the public API.
723 Note: this only returns an empty I<arrayref>. Each class should have its
735 my @unredact_list = @{$object->unredact_list};
737 Generic method that returns the list of database columns that are allowed to
738 be passed to render objects on the API when the user making the request should
739 not ordinarily have unrestricted access to the data (as returned by the is_accesible method).
741 Note: this only returns an empty I<arrayref>. Each class should have its
750 =head3 from_api_mapping
752 my $mapping = $object->from_api_mapping;
754 Generic method that returns the attribute name mappings so the data that
755 comes from the API is correctly renamed to match what is required for the DB.
759 sub from_api_mapping {
762 my $to_api_mapping = $self->to_api_mapping;
764 unless ( defined $self->{_from_api_mapping} ) {
765 $self->{_from_api_mapping} = {};
766 while (my ($key, $value) = each %{ $to_api_mapping } ) {
767 $self->{_from_api_mapping}->{$value} = $key
772 return $self->{_from_api_mapping};
777 my $object = Koha::Object->new_from_api;
778 my $object = Koha::Object->new_from_api( $attrs );
780 Creates a new object, mapping the API attribute names to the ones on the DB schema.
785 my ( $class, $params ) = @_;
787 my $self = $class->new;
788 return $self->set_from_api( $params );
793 my $object = Koha::Object->new(...);
794 $object->set_from_api( $attrs )
796 Sets the object's attributes mapping API attribute names to the ones on the DB schema.
801 my ( $self, $from_api_params ) = @_;
803 return $self->set( $self->attributes_from_api( $from_api_params ) );
806 =head3 attributes_from_api
808 my $attributes = attributes_from_api( $params );
810 Returns the passed params, converted from API naming into the model.
814 sub attributes_from_api {
815 my ( $self, $from_api_params ) = @_;
817 my $from_api_mapping = $self->from_api_mapping;
820 my $columns_info = $self->_result->result_source->columns_info;
821 my $dtf = $self->_result->result_source->storage->datetime_parser;
823 while (my ($key, $value) = each %{ $from_api_params } ) {
824 my $koha_field_name =
825 exists $from_api_mapping->{$key}
826 ? $from_api_mapping->{$key}
829 if ( $columns_info->{$koha_field_name}->{is_boolean} ) {
830 # TODO: Remove when D8 is formally deprecated
831 # Handle booleans gracefully
832 $value = ( $value ) ? 1 : 0;
834 elsif ( _date_or_datetime_column_type( $columns_info->{$koha_field_name}->{data_type} ) ) {
835 if (defined $value) {
837 if ( $columns_info->{$koha_field_name}->{data_type} eq 'date' ) {
838 my $dt = DateTime::Format::MySQL->parse_date($value);
839 $value = $dtf->format_date($dt);
842 my $dt = Koha::DateTime::Format::RFC3339->parse_datetime($value);
843 $value = $dtf->format_datetime($dt);
847 Koha::Exceptions::BadParameter->throw( parameter => $key );
852 $params->{$koha_field_name} = $value;
858 =head3 $object->unblessed_all_relateds
860 my $everything_into_one_hashref = $object->unblessed_all_relateds
862 The unblessed method only retrieves column' values for the column of the object.
863 In a *few* cases we want to retrieve the information of all the prefetched data.
867 sub unblessed_all_relateds {
871 my $related_resultsets = $self->_result->{related_resultsets} || {};
872 my $rs = $self->_result;
873 while ( $related_resultsets and %$related_resultsets ) {
874 my @relations = keys %{ $related_resultsets };
876 my $relation = $relations[0];
877 $rs = $rs->related_resultset($relation)->get_cache;
878 $rs = $rs->[0]; # Does it makes sense to have several values here?
879 my $object_class = Koha::Object::_get_object_class( $rs->result_class );
880 my $koha_object = $object_class->_new_from_dbic( $rs );
881 $related_resultsets = $rs->{related_resultsets};
882 %data = ( %data, %{ $koha_object->unblessed } );
885 %data = ( %data, %{ $self->unblessed } );
889 =head3 $object->_result();
891 Returns the internal DBIC Row object
898 # If we don't have a dbic row at this point, we need to create an empty one
900 Koha::Database->new()->schema()->resultset( $self->_type() )->new({});
902 return $self->{_result};
905 =head3 $object->_columns();
907 Returns an arrayref of the table columns
914 # If we don't have a dbic row at this point, we need to create an empty one
915 $self->{_columns} ||= [ $self->_result()->result_source()->columns() ];
917 return $self->{_columns};
920 sub _get_object_class {
924 if( $type->can('koha_object_class') ) {
925 return $type->koha_object_class;
927 $type =~ s|Schema::Result::||;
933 The autoload method is used only to get and set values for an objects properties.
940 my $method = our $AUTOLOAD;
943 my @columns = @{$self->_columns()};
944 if ( grep { $_ eq $method } @columns ) {
946 # Lazy definition of get/set accessors like $item->barcode; note that it contains $method
950 $self->_result()->set_column( $method, @_ );
953 return $self->_result()->get_column($method);
956 # If called from child class as $self->SUPER-><accessor_name>
957 # $AUTOLOAD will contain ::SUPER which breaks method lookup
958 # therefore we cannot write those entries into the symbol table
959 unless ( $AUTOLOAD =~ /::SUPER::/ ) {
960 no strict 'refs'; ## no critic (strict)
961 *{$AUTOLOAD} = $accessor;
963 return $accessor->( $self, @_ );
966 my @known_methods = qw( is_changed id in_storage get_column discard_changes make_column_dirty );
968 Koha::Exceptions::Object::MethodNotCoveredByTests->throw(
969 error => sprintf("The method %s->%s is not covered by tests!", ref($self), $method),
971 ) unless grep { $_ eq $method } @known_methods;
973 my $r = eval { $self->_result->$method(@_) };
975 Koha::Exceptions::Object->throw( ref($self) . "::$method generated this error: " . $@ );
982 This method must be defined in the child class. The value is the name of the DBIC resultset.
983 For example, for borrowers, the _type method will return "Borrower".
989 =head3 _handle_to_api_child
993 sub _handle_to_api_child {
994 my ($self, $args ) = @_;
996 my $child = $args->{child};
997 my $next = $args->{next};
998 my $curr = $args->{curr};
999 my $params = $args->{params};
1003 if ( defined $child ) {
1005 Koha::Exception->throw( "Asked to embed $curr but its return value doesn't implement to_api" )
1006 if defined $next and blessed $child and !$child->can('to_api');
1008 if ( blessed $child ) {
1009 $params->{embed} = $next;
1010 $res = $child->to_api($params);
1020 =head3 is_accessible
1022 if ( $object->is_accessible ) { ... }
1024 Stub method that is expected to be overloaded (if required) by implementing classes.
1038 Kyle M Hall <kyle@bywatersolutions.com>
1040 Jonathan Druart <jonathan.druart@bugs.koha-community.org>