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 );
29 use Koha
::Exceptions
::Object
;
34 Koha::Object - Koha Object base class
39 my $object = Koha::Object->new({ property1 => $property1, property2 => $property2, etc... } );
43 This class must always be subclassed.
51 =head3 Koha::Object->new();
53 my $object = Koha::Object->new();
54 my $object = Koha::Object->new($attributes);
56 Note that this cannot be used to retrieve record from the DB.
61 my ( $class, $attributes ) = @_;
65 my $schema = Koha
::Database
->new->schema;
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};
77 $schema->resultset( $class->_type() )->new($attributes);
80 croak
("No _type found! Koha::Object must be subclassed!")
81 unless $class->_type();
83 bless( $self, $class );
87 =head3 Koha::Object->_new_from_dbic();
89 my $object = Koha::Object->_new_from_dbic($dbic_row);
94 my ( $class, $dbic_row ) = @_;
98 $self->{_result
} = $dbic_row;
100 croak
("No _type found! Koha::Object must be subclassed!")
101 unless $class->_type();
103 croak
( "DBIC result _type " . ref( $self->{_result
} ) . " isn't of the _type " . $class->_type() )
104 unless ref( $self->{_result
} ) eq "Koha::Schema::Result::" . $class->_type();
106 bless( $self, $class );
110 =head3 $object->store();
112 Saves the object in storage.
113 If the object is new, it will be created.
114 If the object previously existed, it will be updated.
117 $self if the store was a success
118 undef if the store failed
125 my $columns_info = $self->_result->result_source->columns_info;
127 # Handle not null and default values for integers and dates
128 foreach my $col ( keys %{$columns_info} ) {
130 if ( _numeric_column_type
( $columns_info->{$col}->{data_type
} ) ) {
131 # Has been passed but not a number, usually an empty string
132 my $value = $self->_result()->get_column($col);
133 if ( defined $value and not looks_like_number
( $value ) ) {
134 if ( $columns_info->{$col}->{is_nullable
} ) {
135 # If nullable, default to null
136 $self->_result()->set_column($col => undef);
138 # If cannot be null, get the default value
139 # What if cannot be null and does not have a default value? Possible?
140 $self->_result()->set_column($col => $columns_info->{$col}->{default_value
});
144 elsif ( _date_or_datetime_column_type
( $columns_info->{$col}->{data_type
} ) ) {
145 # Set to null if an empty string (or == 0 but should not happen)
146 my $value = $self->_result()->get_column($col);
147 if ( defined $value and not $value ) {
148 if ( $columns_info->{$col}->{is_nullable
} ) {
149 $self->_result()->set_column($col => undef);
151 $self->_result()->set_column($col => $columns_info->{$col}->{default_value
});
154 elsif ( not defined $self->$col
155 && $columns_info->{$col}->{datetime_undef_if_invalid
} )
158 $self->_result()->set_column($col => $columns_info->{$col}->{default_value
});
164 return $self->_result()->update_or_insert() ?
$self : undef;
167 # Catch problems and raise relevant exceptions
168 if (ref($_) eq 'DBIx::Class::Exception') {
170 if ( $_->{msg
} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
172 # FIXME: MySQL error, if we support more DB engines we should implement this for each
173 if ( $_->{msg
} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
174 Koha
::Exceptions
::Object
::FKConstraint
->throw(
175 error
=> 'Broken FK constraint',
176 broken_fk
=> $+{column
}
180 elsif( $_->{msg
} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ ) {
181 Koha
::Exceptions
::Object
::DuplicateID
->throw(
182 error
=> 'Duplicate ID',
183 duplicate_id
=> $+{key
}
186 elsif( $_->{msg
} =~ /Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/ ) { # The optional \W in the regex might be a quote or backtick
188 my $value = $+{value
};
189 my $property = $+{property
};
190 $property =~ s/['`]//g;
191 Koha
::Exceptions
::Object
::BadValue
->throw(
194 property
=> $property =~ /(\w+\.\w+)$/ ?
$1 : $property, # results in table.column without quotes or backtics
198 # Catch-all for foreign key breakages. It will help find other use cases
203 =head3 $object->update();
205 A shortcut for set + store in one call.
210 my ($self, $values) = @_;
211 Koha
::Exceptions
::Object
::NotInStorage
->throw unless $self->in_storage;
212 $self->set($values)->store();
215 =head3 $object->delete();
217 Removes the object from storage.
220 1 if the deletion was a success
221 0 if the deletion failed
222 -1 if the object was never in storage
229 my $deleted = $self->_result()->delete;
230 if ( ref $deleted ) {
231 my $object_class = Koha
::Object
::_get_object_class
( $self->_result->result_class );
232 $deleted = $object_class->_new_from_dbic($deleted);
237 =head3 $object->set( $properties_hashref )
241 property1 => $property1,
242 property2 => $property2,
243 property3 => $propery3,
247 Enables multiple properties to be set at once
250 1 if all properties were set.
251 0 if one or more properties do not exist.
252 undef if all properties exist but a different error
253 prevents one or more properties from being set.
255 If one or more of the properties do not exist,
256 no properties will be set.
261 my ( $self, $properties ) = @_;
263 my @columns = @
{$self->_columns()};
265 foreach my $p ( keys %$properties ) {
266 unless ( grep { $_ eq $p } @columns ) {
267 Koha
::Exceptions
::Object
::PropertyNotFound
->throw( "No property $p for " . ref($self) );
271 return $self->_result()->set_columns($properties) ?
$self : undef;
274 =head3 $object->set_or_blank( $properties_hashref )
276 $object->set_or_blank(
278 property1 => $property1,
279 property2 => $property2,
280 property3 => $propery3,
284 If not listed in $properties_hashref, the property will be set to the default
285 value defined at DB level, or nulled.
291 my ( $self, $properties ) = @_;
293 my $columns_info = $self->_result->result_source->columns_info;
295 foreach my $col ( keys %{$columns_info} ) {
297 next if exists $properties->{$col};
299 if ( $columns_info->{$col}->{is_nullable
} ) {
300 $properties->{$col} = undef;
302 $properties->{$col} = $columns_info->{$col}->{default_value
};
306 return $self->set($properties);
309 =head3 $object->unblessed();
311 Returns an unblessed representation of object.
318 return { $self->_result->get_columns };
321 =head3 $object->get_from_storage;
325 sub get_from_storage
{
326 my ( $self, $attrs ) = @_;
327 my $stored_object = $self->_result->get_from_storage($attrs);
328 return unless $stored_object;
329 my $object_class = Koha
::Object
::_get_object_class
( $self->_result->result_class );
330 return $object_class->_new_from_dbic($stored_object);
333 =head3 $object->TO_JSON
335 Returns an unblessed representation of the object, suitable for JSON output.
343 my $unblessed = $self->unblessed;
344 my $columns_info = Koha
::Database
->new->schema->resultset( $self->_type )
345 ->result_source->{_columns
};
347 foreach my $col ( keys %{$columns_info} ) {
349 if ( $columns_info->{$col}->{is_boolean
} )
350 { # Handle booleans gracefully
352 = ( $unblessed->{$col} )
356 elsif ( _numeric_column_type
( $columns_info->{$col}->{data_type
} )
357 and looks_like_number
( $unblessed->{$col} )
360 # TODO: Remove once the solution for
361 # https://rt.cpan.org/Ticket/Display.html?id=119904
362 # is ported to whatever distro we support by that time
363 $unblessed->{$col} += 0;
365 elsif ( _decimal_column_type
( $columns_info->{$col}->{data_type
} )
366 and looks_like_number
( $unblessed->{$col} )
369 # TODO: Remove once the solution for
370 # https://rt.cpan.org/Ticket/Display.html?id=119904
371 # is ported to whatever distro we support by that time
372 $unblessed->{$col} += 0.00;
374 elsif ( _datetime_column_type
( $columns_info->{$col}->{data_type
} ) ) {
376 return unless $unblessed->{$col};
377 $unblessed->{$col} = output_pref
({
378 dateformat
=> 'rfc3339',
379 dt
=> dt_from_string
($unblessed->{$col}, 'sql'),
387 sub _date_or_datetime_column_type
{
388 my ($column_type) = @_;
396 return ( grep { $column_type eq $_ } @dt_types) ?
1 : 0;
398 sub _datetime_column_type
{
399 my ($column_type) = @_;
406 return ( grep { $column_type eq $_ } @dt_types) ?
1 : 0;
409 sub _numeric_column_type
{
410 # TODO: Remove once the solution for
411 # https://rt.cpan.org/Ticket/Display.html?id=119904
412 # is ported to whatever distro we support by that time
413 my ($column_type) = @_;
415 my @numeric_types = (
424 return ( grep { $column_type eq $_ } @numeric_types) ?
1 : 0;
427 sub _decimal_column_type
{
428 # TODO: Remove once the solution for
429 # https://rt.cpan.org/Ticket/Display.html?id=119904
430 # is ported to whatever distro we support by that time
431 my ($column_type) = @_;
433 my @decimal_types = (
439 return ( grep { $column_type eq $_ } @decimal_types) ?
1 : 0;
442 =head3 prefetch_whitelist
444 my $whitelist = $object->prefetch_whitelist()
446 Returns a hash of prefetchable subs and the type they return.
450 sub prefetch_whitelist
{
454 my $relations = $self->_result->result_source->_relationships;
456 foreach my $key (keys %{$relations}) {
457 if($self->can($key)) {
458 my $result_class = $relations->{$key}->{class};
459 my $obj = $result_class->new;
461 $whitelist->{$key} = Koha
::Object
::_get_object_class
( $obj->result_class );
463 $whitelist->{$key} = undef;
473 my $object_for_api = $object->to_api(
494 Returns a representation of the object, suitable for API output.
499 my ( $self, $params ) = @_;
500 my $json_object = $self->TO_JSON;
502 my $to_api_mapping = $self->to_api_mapping;
504 # Rename attributes if there's a mapping
505 if ( $self->can('to_api_mapping') ) {
506 foreach my $column ( keys %{ $self->to_api_mapping } ) {
507 my $mapped_column = $self->to_api_mapping->{$column};
508 if ( exists $json_object->{$column}
509 && defined $mapped_column )
512 $json_object->{$mapped_column} = delete $json_object->{$column};
514 elsif ( exists $json_object->{$column}
515 && !defined $mapped_column )
518 delete $json_object->{$column};
523 my $embeds = $params->{embed
};
526 foreach my $embed ( keys %{$embeds} ) {
527 if ( $embed =~ m/^(?<relation>.*)_count$/
528 and $embeds->{$embed}->{is_count
} ) {
530 my $relation = $+{relation
};
531 $json_object->{$embed} = $self->$relation->count;
535 my $next = $embeds->{$curr}->{children
};
537 my $children = $self->$curr;
539 if ( defined $children and ref($children) eq 'ARRAY' ) {
541 $self->_handle_to_api_child(
542 { child
=> $_, next => $next, curr
=> $curr } )
544 $json_object->{$curr} = \
@list;
547 $json_object->{$curr} = $self->_handle_to_api_child(
548 { child
=> $children, next => $next, curr
=> $curr } );
559 =head3 to_api_mapping
561 my $mapping = $object->to_api_mapping;
563 Generic method that returns the attribute name mappings required to
564 render the object on the API.
566 Note: this only returns an empty I<hashref>. Each class should have its
567 own mapping returned.
575 =head3 from_api_mapping
577 my $mapping = $object->from_api_mapping;
579 Generic method that returns the attribute name mappings so the data that
580 comes from the API is correctly renamed to match what is required for the DB.
584 sub from_api_mapping
{
587 my $to_api_mapping = $self->to_api_mapping;
589 unless ( $self->{_from_api_mapping
} ) {
590 while (my ($key, $value) = each %{ $to_api_mapping } ) {
591 $self->{_from_api_mapping
}->{$value} = $key
596 return $self->{_from_api_mapping
};
601 my $object = Koha::Object->new_from_api;
602 my $object = Koha::Object->new_from_api( $attrs );
604 Creates a new object, mapping the API attribute names to the ones on the DB schema.
609 my ( $class, $params ) = @_;
611 my $self = $class->new;
612 return $self->set_from_api( $params );
617 my $object = Koha::Object->new(...);
618 $object->set_from_api( $attrs )
620 Sets the object's attributes mapping API attribute names to the ones on the DB schema.
625 my ( $self, $from_api_params ) = @_;
627 return $self->set( $self->attributes_from_api( $from_api_params ) );
630 =head3 attributes_from_api
632 my $attributes = attributes_from_api( $params );
634 Returns the passed params, converted from API naming into the model.
638 sub attributes_from_api
{
639 my ( $self, $from_api_params ) = @_;
641 my $from_api_mapping = $self->from_api_mapping;
644 my $columns_info = $self->_result->result_source->columns_info;
646 while (my ($key, $value) = each %{ $from_api_params } ) {
647 my $koha_field_name =
648 exists $from_api_mapping->{$key}
649 ?
$from_api_mapping->{$key}
652 if ( $columns_info->{$koha_field_name}->{is_boolean
} ) {
653 # TODO: Remove when D8 is formally deprecated
654 # Handle booleans gracefully
655 $value = ( $value ) ?
1 : 0;
657 elsif ( _date_or_datetime_column_type
( $columns_info->{$koha_field_name}->{data_type
} ) ) {
659 $value = dt_from_string
($value, 'rfc3339');
662 Koha
::Exceptions
::BadParameter
->throw( parameter
=> $key );
666 $params->{$koha_field_name} = $value;
672 =head3 $object->unblessed_all_relateds
674 my $everything_into_one_hashref = $object->unblessed_all_relateds
676 The unblessed method only retrieves column' values for the column of the object.
677 In a *few* cases we want to retrieve the information of all the prefetched data.
681 sub unblessed_all_relateds
{
685 my $related_resultsets = $self->_result->{related_resultsets
} || {};
686 my $rs = $self->_result;
687 while ( $related_resultsets and %$related_resultsets ) {
688 my @relations = keys %{ $related_resultsets };
690 my $relation = $relations[0];
691 $rs = $rs->related_resultset($relation)->get_cache;
692 $rs = $rs->[0]; # Does it makes sense to have several values here?
693 my $object_class = Koha
::Object
::_get_object_class
( $rs->result_class );
694 my $koha_object = $object_class->_new_from_dbic( $rs );
695 $related_resultsets = $rs->{related_resultsets
};
696 %data = ( %data, %{ $koha_object->unblessed } );
699 %data = ( %data, %{ $self->unblessed } );
703 =head3 $object->_result();
705 Returns the internal DBIC Row object
712 # If we don't have a dbic row at this point, we need to create an empty one
714 Koha
::Database
->new()->schema()->resultset( $self->_type() )->new({});
716 return $self->{_result
};
719 =head3 $object->_columns();
721 Returns an arrayref of the table columns
728 # If we don't have a dbic row at this point, we need to create an empty one
729 $self->{_columns
} ||= [ $self->_result()->result_source()->columns() ];
731 return $self->{_columns
};
734 sub _get_object_class
{
738 if( $type->can('koha_object_class') ) {
739 return $type->koha_object_class;
741 $type =~ s
|Schema
::Result
::||;
747 The autoload method is used only to get and set values for an objects properties.
754 my $method = our $AUTOLOAD;
757 my @columns = @
{$self->_columns()};
758 # Using direct setter/getter like $item->barcode() or $item->barcode($barcode);
759 if ( grep { $_ eq $method } @columns ) {
761 $self->_result()->set_column( $method, @_ );
764 my $value = $self->_result()->get_column( $method );
769 my @known_methods = qw( is_changed id in_storage get_column discard_changes make_column_dirty );
771 Koha
::Exceptions
::Object
::MethodNotCoveredByTests
->throw(
772 error
=> sprintf("The method %s->%s is not covered by tests!", ref($self), $method),
774 ) unless grep { $_ eq $method } @known_methods;
777 my $r = eval { $self->_result->$method(@_) };
779 Koha
::Exceptions
::Object
->throw( ref($self) . "::$method generated this error: " . $@
);
786 This method must be defined in the child class. The value is the name of the DBIC resultset.
787 For example, for borrowers, the _type method will return "Borrower".
793 =head3 _handle_to_api_child
797 sub _handle_to_api_child
{
798 my ($self, $args ) = @_;
800 my $child = $args->{child
};
801 my $next = $args->{next};
802 my $curr = $args->{curr
};
806 if ( defined $child ) {
808 Koha
::Exceptions
::Exception
->throw( "Asked to embed $curr but its return value doesn't implement to_api" )
809 if defined $next and blessed
$child and !$child->can('to_api');
811 if ( blessed
$child ) {
812 $res = $child->to_api({ embed
=> $next });
826 Kyle M Hall <kyle@bywatersolutions.com>
828 Jonathan Druart <jonathan.druart@bugs.koha-community.org>