Tony Tony - 5 days ago 5
Perl Question

Using of standalone model in Catalyst - calling of general methods

I am following receipt of using standalone models in Catalyst in the advent calendar from 2012 here:

http://www.catalystframework.org/calendar/2012/15
and previous days ....

Following this receipt I am transferring existing code from current fat model. I want to have some methods in Model on general level - I expect to put them in this example into

lib/StandaloneApp3.pm
file (eg. create order means to create record in table order_header, table order_item and table log) and call them from Controller with code:

$c->model('DB')->create_order($params);


I am getting error:


"Can't locate object method "create_order" via package
"WebApp::Model::DB"


When calling function from StandaloneApp3 namespace, this work:

&StandaloneApp3::create_order($params);


I do not think this is good approach as my Controller should connect only to Catalyst Model and not directly to standalone library.

Is there an error in my code to access methods in StandaloneApp3.pm or when using DBIC::Schema I am not expect to call this method? In case I am not supposed to call methods in StandaloneApp3.pm, what is the correct place were to store methods that can write to more tables in one transaction? Any example also for exact calling those methods from outside Catalyst - eg. from command line?

Thank you for your explanation and example.

----------- ADDED CODE -------------

Catalyst model:
file: lib\WebApp\Model\DB.pm:

package WebApp::Model::DB;
use strict;
use base 'Catalyst::Model::DBIC::Schema';
1;


Standalone Model:
file: lib/StandaloneApp3.pm

use utf8;
package StandaloneApp3;
use Moose;
use MooseX::MarkAsMethods autoclean => 1;
extends 'DBIx::Class::Schema';
PACKAGE->load_namespaces;
sub create_order {
# in development
return "order_created";
}
PACKAGE->meta->make_immutable(inline_constructor => 0);
1;


Config:
file: webapp.conf

<Model::DB>
schema_class StandaloneApp3
<connect_info>
dsn dbi:SQLite:__path_to(data,database_file2.db)__
</connect_info>
</Model::DB>


Thank you

Answer

You are using StandaloneApp3 as your schema class inside of the catalyst model WebApp::Model::DB. That means it's the DBIx::Class schema that is automatically loaded for you. The schema is something like a database object.

To access the schema, you use the schema accessor method on the model.

$c->model('DB')->schema;

So to reiterate, the schema is not part of the model. The model does not inherit from the schema. It just has the schema as an attribute.

Therefore the method create_order in the schema class StandaloneApp3 is not a method on the model WebApp::Model::DB. It's a method on its schema attribute.

$c->model('DB')->schema->create_order;

The easiest way to patch that through would be to make a method create_order in WebApp::Model::DB that calls the method with the same name on the schema.

package WebApp::Model::DB;
# ...

sub create_order {
    my $self = shift;

    return $self->schema->create_order(@_);
}

I shifted out the $self so I can pass on the rest of the argument list @_ to $self->schema->create_order.

To prove this works, I wrote create_order in StandaloneApp3 like this:

package StandaloneApp3;
# ...

# You can replace this text with custom code or comments, and it will be preserved on regeneration

sub create_order {
    # in development

    warn "creating order...";
    return "order_created"; 
}

I put the warn in so we can find it in the log that you have in your console later. Note how it's placed underneath DBIC's comment, because we don't want it to be overwritten if we need to regenerate the schema class later in case someone made changes to the database schema.

Finally I'm calling the model method from sub index in the Root controller.

sub index :Path :Args(0) {
    my ( $self, $c ) = @_;

    $c->model('DB')->create_order;

    # Hello World
    $c->response->body( $c->welcome_message );
}

If you now request / in your browser (for me that's http://localhost:5000/) now it works, and shows up in the log. Mine is on Windows so it looks a bit odd.

[info] *** Request 1 (0.143/s) [8012] [Sun Nov 27 16:25:59 2016] ***
[debug] Path is "/"
[debug] "GET" request for "/" from "127.0.0.1"
creating order... at lib/StandaloneApp3.pm line 23.
127.0.0.1 - - [27/Nov/2016:16:25:59 +0100] "GET / HTTP/1.1" 200 5477 "-" "Mozill
a/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0
.2840.99 Safari/537.36"
[debug] Response Code: 200; Content-Type: text/html; charset=utf-8; Content-Leng
th: unknown
[info] Request took 0.012028s (83.139/s)
.------------------------------------------------------------+-----------.
| Action                                                     | Time      |
+------------------------------------------------------------+-----------+
| /index                                                     | 0.000545s |
| /end                                                       | 0.000340s |
'------------------------------------------------------------+-----------'

But of course if you have a couple of those methods you want to patch through, it gets a bit tedious to write all of them yourself. You could use AUTOLOAD to create methods on the fly, but that seems wrong in this situation, so I'm not going to explain that.

Instead, let's use Catalyst::TraitFor::Model::DBIC::Schema::SchemaProxy, which is a model trait that's included with Catalyst::Model::DBIC::Schema for exactly this situation.

You load it from the configuration file or inside the __PACKAGE__->config(...) in your code. I prefer it there, because it's easier to see what's going on when reading the code, and you can still overwrite it later in the configuration files.

Here is the full WebApp::Model::DB file. I removed the method create_order we put there in the example above, and included the trait configuration instead.

package WebApp::Model::DB;
use strict;
use base 'Catalyst::Model::DBIC::Schema';

__PACKAGE__->config(
    traits => 'SchemaProxy',
);

1;

That's all you need to change. If you restart the app and request the root / again, it will still work.

[info] *** Request 1 (0.111/s) [6160] [Sun Nov 27 16:43:45 2016] ***
[debug] Path is "/"
[debug] "GET" request for "/" from "127.0.0.1"
creating order... at lib/StandaloneApp3.pm line 23.
127.0.0.1 - - [27/Nov/2016:16:43:45 +0100] "GET / HTTP/1.1" 200 5477 "-" "Mozill
a/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0
.2840.99 Safari/537.36"
[debug] Response Code: 200; Content-Type: text/html; charset=utf-8; Content-Leng
th: unknown
[info] Request took 0.011867s (84.267/s)
.------------------------------------------------------------+-----------.
| Action                                                     | Time      |
+------------------------------------------------------------+-----------+
| /index                                                     | 0.000712s |
| /end                                                       | 0.000370s |
'------------------------------------------------------------+-----------'

I suggest you take a few minutes and read the CPAN pages of the modules I linked in this answer. I just did the same. Catalyst can become very complex underneath and it's hard to remember all of it even if you use it a lot, so freshening up on the docs once in a while is a good idea.

Comments