Koha Objects

From Koha Wiki
Jump to navigation Jump to search

 Koha Objects

This page is mean to be an introduction to working with Koha::Object and Koha::Objects. ( Community Bug 13019 )

Database Table Conventions

Table naming should follow the following conventions:

  • Table names should be plural ( e.g. widgets, not widget )
  • Tables should be named in a lower case letters ( e.g. my_table, not MyTable )
  • Table names consisting of multiple words should have those words separated by underscores ( e.g. my_table, not myTable )
  • Every table should have a single column primary key with the name id
  • Every foreign key should have the same name as the primary key it links to when possible ( exceptions would be multiple FKs to the same table, and where a descriptor is better e.g. widgets.borrowernumber = widgets.managing_borrowernumber )

Using Koha::Object and Koha::Objects

An instance of Koha::Object, in general terms, represents a single row in a single table of the database.

An instance of Koha::Objects, in general terms, represents a collection of rows in a single table of the database ( i.e. a collection of Koha::Object ).

Learning by example

Let us imagine that we need to add support for widgets, such that each Koha borrower can have any number of widgets attached to his or her record.

Creating the table

First, we need to create our widget table:

CREATE TABLE borrower_widgets (
`id` INT( 11 ) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`borrowernumber` INT( 11 ) NOT NULL ,
`value` VARCHAR( 255 ) NOT NULL ,
INDEX ( `borrowernumber` )
) ENGINE = INNODB;

ALTER TABLE  `borrower_widgets` ADD FOREIGN KEY (  `borrowernumber` ) REFERENCES  `borrowers` (
`borrowernumber`
) ON DELETE CASCADE ON UPDATE CASCADE ;

Once we have created our table, we will need to run misc/devel/update_dbix_class_files.pl to update our DBIx::Class files. We will need to know our dbic resultset name for the next step.

Creating the Koha::Borrower::Widget class

package Koha::Borrower::Widget;

use Modern::Perl;

use Koha::Database;

use base qw(Koha::Object);

sub _type {
    return 'BorrowerWidget';
}

1;

That's it! The method '_type' should always return the DBIx::Class resultset type for this type of object. Note also, we can name the package however we wish. Instead of Koha::BorrowerWidget, we use the much more sensible package name Koha::Borrower::Widget.

Creating the Koha::Borrower::Widgets class

package Koha::Borrower::Widgets;

use Modern::Perl;

use Koha::Database;

use Koha::Borrower;

use base qw(Koha::Objects);

sub _type {
    return 'BorrowerWidget';
}

sub object_class {
    return 'Koha::Borrower::Widget';
}

1;

You can see here that Koha::Borrower::Widgets looks very similar to Koha::Borrower::Widget, expect the base class is now Koha::Objects and it has one more method 'object_class' which always returns the singular version of our widgets ( i.e. Koha::Widget ).

Using our widgets

With only this code, we can already begin using our widgets! Keep in mind that for using plural and singular classes, only the plural needs to be included:

use Koha::Borrower::Widgets;

We can create a new widget and save it to the database like this:

my $widget = Koha::Borrower::widget->new({ borrowernumber => $borrowernumber, value => $value })->store();

We can find a single widget or collection of widgets with Koha::Borrower::Widgets

# Find a single widget
my $widget = Koha::Borrower::Widgets->find( $id );

# Find a collection of widgets as an array
my @widgets = Koha::Borrower::Widgets->search({ borrowernumber => $borrowernumber });
map { warn "ID: " . $_->id } @widgets;

# Find a collection of widgest as a Set
my $widgets = Koha::Borrower::Widgets->search({ borrowernumber => $borrowernumber });
while ( my $w = $widgets->next() ) {
    warn "ID: " . $w->id(); # The id() method will always return the primary key no matter what it's column name is
}

Extending our widgets

Let's assume we already have a borrower object. By adding the following method to it

sub widgets {
    my ($self) = @_;

    return Koha::Borrower::Widgets->search( { borrowernumber => $self->borrowernumber() } );
}vi 

How we can get all our widgets from our borrower!

my @widgets = $borrower->widgets(); # as an array
my $widgets = $borrower->widgets(); # as an object set

We can also utilize DBIx::Class directly from our object. The following method could be added to Koha::Borrower::Widget to get the 'owner' of a given widget:

sub borrower {
    my ($self) = @_;

    my $dbic_borrower = $self->_result()->borrowernumber();

    return Koha::Borrower->new_from_dbic( $dbic_borrower );
}

While this example is uselessly simplistic, it does show you can access your DBIC classes for more advanced searching and filtering, which could be useful in special methods ( for example, methods that act as canned searches to keep the code DRY ).

Best Practices

All method names should use snake_case.

All methods should return $self whenever possible to allow for chaining of methods.

For new methods, don't pass object id's, pass the object itself ( $object->method( $another_object ), not $object->method( $another_object_id ) ). By passing the objects around we avoid refetching data from the database that we already have!

If passing a single object isn't appropriate, all parameters should be passed in as a single hashref ( $object->method( { param1 => $param1, param2 => $params2 } ).

Use Carp instead of warns. Avoid returning undef without carping. For example:

sub frobnicate {
    my ( $self, $params ) = @_;

    my $foo = $params->{foo};
    my $bar = $params->{bar};

    unless ( $foo && $bar ) {
        carp("Object::frobnicate - parameter foo not passed in!") unless $foo;
        carp("Object::frobnicate - parameter bar not passed in!") unless $bar;
        return;
    }

    // Do stuff

    return $self;
}

Subclassing Koha::Object and Object "factories"

Koha::Objects are so awesome you can even subclass them to state a specific purpose, while retaining the same database format than the parent class.

Let us imagine we wanted to improve the messaging module of Koha, by first creating a core Koha::MessageQueue -object. Then we could create objects for specific message types, like

  • Koha::MessageQueue::Notification::Overdue
  • Koha::MessageQueue::Notification::HoldReadyForPickup
  • Koha::MessageQueue::Notification::PurchaseSuggestion

If we could know which type of message we are sending, we can easily target special rules for the transportation, like creating a fine or using a different SMS::Send::Driver for different kinds of notifications.


Let us imagine that we already have a base class Koha::MessageQueue defined, it looks like this:

package Koha::MessageQueue;

use Modern::Perl;

use Koha::Database;

use base qw(Koha::Object);

sub _type {
    return 'MessageQueue';
}
1;

And the factory class for it

package Koha::MessageQueues;

use Modern::Perl;

use Koha::Database;

use Koha::MessageQueue;

use base qw(Koha::Objects);

sub _type {
    return 'MessageQueue';
}

sub object_class {
    return 'Koha::MessageQueue';
}
1;

To subclass the Koha::MessageQueue to Koha::MessageQueue::Notification::Overdue, create the following file:

package Koha::MessageQueue::Notification::Overdue;

use Modern::Perl;

use Koha::Database;

use base qw(Koha::MessageQueue); #subclass the parent

sub _type {
    #It is important to return the same type, because this infers the DBIx-schema to use.
    #We want to use the same DB tables, but provide different behaviour.
    return 'MessageQueue';
}

#Add some behaviour to the subclass we don't need elsewhere.
sub fine {
    #Find the fine from the overduerules
    return 3.5;
}
1;

We need to create the factory class as well, but it is not so much of work. Here we can overload the find() and search() methods to narrow the DBIx searches to only match the criteria we know identifies the subclassed business objects. Otherwise all find() requests could return objects whom really are not Overdue-objects, but any MessageQueue-objects. This might cause bad side-effects.

package Koha::MessageQueue::Notification::Overdues;

use Modern::Perl;

use Koha::Database;

use Koha::MessageQueue::Notification::Overdue;

use base qw(Koha::MessageQueues);

sub _type {
    #This defines the DBIx-schema to use
    return 'MessageQueue';
}

sub object_class {
    #This sets the class to bless the object reference
    return 'Koha::MessageQueue::Notification::Overdue';
}

#Overload the Koha::Objects find() to add extra checks to return only correct
#MessageQueue-objects as Overdue-objects.
sub find {
    my ( $self, $id ) = @_;

    return unless $id;

    #Lets pretend we know how to decide which message_queue-rows (MessageQueue-objects) are actually
    #Overdue-objects, we can infer it from the letter_code for example.
    my $letterCodes = $overdueRulesMap->getLetterCodes();
    my $result = $self->_resultset()->find({message_id => $id, letter_code => { -in => $letterCodes }});
    #So instead of finding with primary key, we make extra checks to make sure this object is really
    #an Overdue.

    my $object = $self->object_class()->_new_from_dbic( $result );

    return $object;
}

#Override Koha::Objects->search()
#Make sure that these searches only match valid Overdue Notifications
sub search {
    my ( $self, $params ) = @_;

    #Inject the Query to filter out only Overdue Notifications
    #Lets pretend we know how to decide which message_queue-rows (MessageQueue-objects) are actually
    #Overdue-objects, we can infer it from the letter_code for example.
    my $letterCodes = $overdueRulesMap->getLetterCodes();
    $params->{letter_code} = {'-in' => $letterCodes};

    return $self->SUPER::search($params);
}
1;

Now to create such an object and access the special behaviour.

my $overdueNotification = Koha::MessageQueue::Notification::Overdues->new();
print $overdueNotification->fine(); #Prints 3.5

#If you have a specific message_queue-row you know is an Overdue, you can do it like this.
my $message = Koha::MessageQueue::Notification::Overdues->find(4303621);
print $message->fine();

#Or even search!
my @messages = Koha::MessageQueue::Notification::Overdues->search(
                                                     {time_queued => {'>' => '2015-01-01'}}
                                                 );
print $message[0]->fine();

Caveats

store doesn't return the DB object

When you create a new object of a Koha::Object-derived class, you are most likely writing like this (expected):

    my $object = Koha::MyClass->new({ attribute_1 => $attribute_1_value, ... , attribute_n => $attribute_value_n })->store;

if some of the attributes are calculated by the DB engine (auto_increment fields, timestamps, default values, DBIC filters, etc) then $object won't have those calculated values. In order to have those available immediatelyyour code needs to look like this:

    my $object = Koha::MyClass->new({ attribute_1 => $attribute_1_value, ... , attribute_n => $attribute_value_n })->store;
    $object->discard_changes;

discard_changes is a DBIC method Koha::Object inherits. Overall, this problem is also inherited from DBIC. There are good reasons for this: it requires an extra query (SELECT) and processing it. A discussion about this took place. The consensus was that most of the time we don't reuse the resulting object, and thus the overhead of an implicit fetch is not worth.


Developer handbook