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') {
169 if ( $_->{msg
} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
171 # FIXME: MySQL error, if we support more DB engines we should implement this for each
172 if ( $_->{msg
} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
173 Koha
::Exceptions
::Object
::FKConstraint
->throw(
174 error
=> 'Broken FK constraint',
175 broken_fk
=> $+{column
}
179 elsif( $_->{msg
} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ ) {
180 Koha
::Exceptions
::Object
::DuplicateID
->throw(
181 error
=> 'Duplicate ID',
182 duplicate_id
=> $+{key
}
185 elsif( $_->{msg
} =~ /Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/ ) { # The optional \W in the regex might be a quote or backtick
187 my $value = $+{value
};
188 my $property = $+{property
};
189 $property =~ s/['`]//g;
190 Koha
::Exceptions
::Object
::BadValue
->throw(
193 property
=> $property =~ /(\w+\.\w+)$/ ?
$1 : $property, # results in table.column without quotes or backtics
197 # Catch-all for foreign key breakages. It will help find other use cases
202 =head3 $object->update();
204 A shortcut for set + store in one call.
209 my ($self, $values) = @_;
210 return $self->set($values)->store();
213 =head3 $object->delete();
215 Removes the object from storage.
218 1 if the deletion was a success
219 0 if the deletion failed
220 -1 if the object was never in storage
227 my $deleted = $self->_result()->delete;
228 if ( ref $deleted ) {
229 my $object_class = Koha
::Object
::_get_object_class
( $self->_result->result_class );
230 $deleted = $object_class->_new_from_dbic($deleted);
235 =head3 $object->set( $properties_hashref )
239 property1 => $property1,
240 property2 => $property2,
241 property3 => $propery3,
245 Enables multiple properties to be set at once
248 1 if all properties were set.
249 0 if one or more properties do not exist.
250 undef if all properties exist but a different error
251 prevents one or more properties from being set.
253 If one or more of the properties do not exist,
254 no properties will be set.
259 my ( $self, $properties ) = @_;
261 my @columns = @
{$self->_columns()};
263 foreach my $p ( keys %$properties ) {
264 unless ( grep { $_ eq $p } @columns ) {
265 Koha
::Exceptions
::Object
::PropertyNotFound
->throw( "No property $p for " . ref($self) );
269 return $self->_result()->set_columns($properties) ?
$self : undef;
272 =head3 $object->set_or_blank( $properties_hashref )
274 $object->set_or_blank(
276 property1 => $property1,
277 property2 => $property2,
278 property3 => $propery3,
282 If not listed in $properties_hashref, the property will be set to the default
283 value defined at DB level, or nulled.
289 my ( $self, $properties ) = @_;
291 my $columns_info = $self->_result->result_source->columns_info;
293 foreach my $col ( keys %{$columns_info} ) {
295 next if exists $properties->{$col};
297 if ( $columns_info->{$col}->{is_nullable
} ) {
298 $properties->{$col} = undef;
300 $properties->{$col} = $columns_info->{$col}->{default_value
};
304 return $self->set($properties);
307 =head3 $object->unblessed();
309 Returns an unblessed representation of object.
316 return { $self->_result->get_columns };
319 =head3 $object->get_from_storage;
323 sub get_from_storage
{
324 my ( $self, $attrs ) = @_;
325 my $stored_object = $self->_result->get_from_storage($attrs);
326 return unless $stored_object;
327 my $object_class = Koha
::Object
::_get_object_class
( $self->_result->result_class );
328 return $object_class->_new_from_dbic($stored_object);
331 =head3 $object->TO_JSON
333 Returns an unblessed representation of the object, suitable for JSON output.
341 my $unblessed = $self->unblessed;
342 my $columns_info = Koha
::Database
->new->schema->resultset( $self->_type )
343 ->result_source->{_columns
};
345 foreach my $col ( keys %{$columns_info} ) {
347 if ( $columns_info->{$col}->{is_boolean
} )
348 { # Handle booleans gracefully
350 = ( $unblessed->{$col} )
354 elsif ( _numeric_column_type
( $columns_info->{$col}->{data_type
} )
355 and looks_like_number
( $unblessed->{$col} )
358 # TODO: Remove once the solution for
359 # https://rt.cpan.org/Ticket/Display.html?id=119904
360 # is ported to whatever distro we support by that time
361 $unblessed->{$col} += 0;
363 elsif ( _datetime_column_type
( $columns_info->{$col}->{data_type
} ) ) {
365 return unless $unblessed->{$col};
366 $unblessed->{$col} = output_pref
({
367 dateformat
=> 'rfc3339',
368 dt
=> dt_from_string
($unblessed->{$col}, 'sql'),
376 sub _date_or_datetime_column_type
{
377 my ($column_type) = @_;
385 return ( grep { $column_type eq $_ } @dt_types) ?
1 : 0;
387 sub _datetime_column_type
{
388 my ($column_type) = @_;
395 return ( grep { $column_type eq $_ } @dt_types) ?
1 : 0;
398 sub _numeric_column_type
{
399 # TODO: Remove once the solution for
400 # https://rt.cpan.org/Ticket/Display.html?id=119904
401 # is ported to whatever distro we support by that time
402 my ($column_type) = @_;
404 my @numeric_types = (
416 return ( grep { $column_type eq $_ } @numeric_types) ?
1 : 0;
419 =head3 prefetch_whitelist
421 my $whitelist = $object->prefetch_whitelist()
423 Returns a hash of prefetchable subs and the type they return.
427 sub prefetch_whitelist
{
431 my $relations = $self->_result->result_source->_relationships;
433 foreach my $key (keys %{$relations}) {
434 if($self->can($key)) {
435 my $result_class = $relations->{$key}->{class};
436 my $obj = $result_class->new;
438 $whitelist->{$key} = $obj->koha_object_class;
440 $whitelist->{$key} = undef;
450 my $object_for_api = $object->to_api(
471 Returns a representation of the object, suitable for API output.
476 my ( $self, $params ) = @_;
477 my $json_object = $self->TO_JSON;
479 my $to_api_mapping = $self->to_api_mapping;
481 # Rename attributes if there's a mapping
482 if ( $self->can('to_api_mapping') ) {
483 foreach my $column ( keys %{ $self->to_api_mapping } ) {
484 my $mapped_column = $self->to_api_mapping->{$column};
485 if ( exists $json_object->{$column}
486 && defined $mapped_column )
489 $json_object->{$mapped_column} = delete $json_object->{$column};
491 elsif ( exists $json_object->{$column}
492 && !defined $mapped_column )
495 delete $json_object->{$column};
500 my $embeds = $params->{embed
};
503 foreach my $embed ( keys %{$embeds} ) {
504 if ( $embed =~ m/^(?<relation>.*)_count$/
505 and $embeds->{$embed}->{is_count
} ) {
507 my $relation = $+{relation
};
508 $json_object->{$embed} = $self->$relation->count;
512 my $next = $embeds->{$curr}->{children
};
514 my $children = $self->$curr;
516 if ( defined $children and ref($children) eq 'ARRAY' ) {
518 $self->_handle_to_api_child(
519 { child
=> $_, next => $next, curr
=> $curr } )
521 $json_object->{$curr} = \
@list;
524 $json_object->{$curr} = $self->_handle_to_api_child(
525 { child
=> $children, next => $next, curr
=> $curr } );
536 =head3 to_api_mapping
538 my $mapping = $object->to_api_mapping;
540 Generic method that returns the attribute name mappings required to
541 render the object on the API.
543 Note: this only returns an empty I<hashref>. Each class should have its
544 own mapping returned.
552 =head3 from_api_mapping
554 my $mapping = $object->from_api_mapping;
556 Generic method that returns the attribute name mappings so the data that
557 comes from the API is correctly renamed to match what is required for the DB.
561 sub from_api_mapping
{
564 my $to_api_mapping = $self->to_api_mapping;
566 unless ( $self->{_from_api_mapping
} ) {
567 while (my ($key, $value) = each %{ $to_api_mapping } ) {
568 $self->{_from_api_mapping
}->{$value} = $key
573 return $self->{_from_api_mapping
};
578 my $object = Koha::Object->new_from_api;
579 my $object = Koha::Object->new_from_api( $attrs );
581 Creates a new object, mapping the API attribute names to the ones on the DB schema.
586 my ( $class, $params ) = @_;
588 my $self = $class->new;
589 return $self->set_from_api( $params );
594 my $object = Koha::Object->new(...);
595 $object->set_from_api( $attrs )
597 Sets the object's attributes mapping API attribute names to the ones on the DB schema.
602 my ( $self, $from_api_params ) = @_;
604 return $self->set( $self->attributes_from_api( $from_api_params ) );
607 =head3 attributes_from_api
609 my $attributes = attributes_from_api( $params );
611 Returns the passed params, converted from API naming into the model.
615 sub attributes_from_api
{
616 my ( $self, $from_api_params ) = @_;
618 my $from_api_mapping = $self->from_api_mapping;
621 my $columns_info = $self->_result->result_source->columns_info;
623 while (my ($key, $value) = each %{ $from_api_params } ) {
624 my $koha_field_name =
625 exists $from_api_mapping->{$key}
626 ?
$from_api_mapping->{$key}
629 if ( $columns_info->{$koha_field_name}->{is_boolean
} ) {
630 # TODO: Remove when D8 is formally deprecated
631 # Handle booleans gracefully
632 $value = ( $value ) ?
1 : 0;
634 elsif ( _date_or_datetime_column_type
( $columns_info->{$koha_field_name}->{data_type
} ) ) {
636 $value = dt_from_string
($value, 'rfc3339');
639 Koha
::Exceptions
::BadParameter
->throw( parameter
=> $key );
643 $params->{$koha_field_name} = $value;
649 =head3 $object->unblessed_all_relateds
651 my $everything_into_one_hashref = $object->unblessed_all_relateds
653 The unblessed method only retrieves column' values for the column of the object.
654 In a *few* cases we want to retrieve the information of all the prefetched data.
658 sub unblessed_all_relateds
{
662 my $related_resultsets = $self->_result->{related_resultsets
} || {};
663 my $rs = $self->_result;
664 while ( $related_resultsets and %$related_resultsets ) {
665 my @relations = keys %{ $related_resultsets };
667 my $relation = $relations[0];
668 $rs = $rs->related_resultset($relation)->get_cache;
669 $rs = $rs->[0]; # Does it makes sense to have several values here?
670 my $object_class = Koha
::Object
::_get_object_class
( $rs->result_class );
671 my $koha_object = $object_class->_new_from_dbic( $rs );
672 $related_resultsets = $rs->{related_resultsets
};
673 %data = ( %data, %{ $koha_object->unblessed } );
676 %data = ( %data, %{ $self->unblessed } );
680 =head3 $object->_result();
682 Returns the internal DBIC Row object
689 # If we don't have a dbic row at this point, we need to create an empty one
691 Koha
::Database
->new()->schema()->resultset( $self->_type() )->new({});
693 return $self->{_result
};
696 =head3 $object->_columns();
698 Returns an arrayref of the table columns
705 # If we don't have a dbic row at this point, we need to create an empty one
706 $self->{_columns
} ||= [ $self->_result()->result_source()->columns() ];
708 return $self->{_columns
};
711 sub _get_object_class
{
715 if( $type->can('koha_object_class') ) {
716 return $type->koha_object_class;
718 $type =~ s
|Schema
::Result
::||;
724 The autoload method is used only to get and set values for an objects properties.
731 my $method = our $AUTOLOAD;
734 my @columns = @
{$self->_columns()};
735 # Using direct setter/getter like $item->barcode() or $item->barcode($barcode);
736 if ( grep { $_ eq $method } @columns ) {
738 $self->_result()->set_column( $method, @_ );
741 my $value = $self->_result()->get_column( $method );
746 my @known_methods = qw( is_changed id in_storage get_column discard_changes make_column_dirty );
748 Koha
::Exceptions
::Object
::MethodNotCoveredByTests
->throw(
749 error
=> sprintf("The method %s->%s is not covered by tests!", ref($self), $method),
751 ) unless grep { $_ eq $method } @known_methods;
754 my $r = eval { $self->_result->$method(@_) };
756 Koha
::Exceptions
::Object
->throw( ref($self) . "::$method generated this error: " . $@
);
763 This method must be defined in the child class. The value is the name of the DBIC resultset.
764 For example, for borrowers, the _type method will return "Borrower".
770 =head3 _handle_to_api_child
774 sub _handle_to_api_child
{
775 my ($self, $args ) = @_;
777 my $child = $args->{child
};
778 my $next = $args->{next};
779 my $curr = $args->{curr
};
783 if ( defined $child ) {
785 Koha
::Exceptions
::Exception
->throw( "Asked to embed $curr but its return value doesn't implement to_api" )
786 if defined $next and blessed
$child and !$child->can('to_api');
788 if ( blessed
$child ) {
789 $res = $child->to_api({ embed
=> $next });
803 Kyle M Hall <kyle@bywatersolutions.com>
805 Jonathan Druart <jonathan.druart@bugs.koha-community.org>