package Dancer2::Plugin::Interchange; use strict; use warnings; use Interchange::Account::Manager; use Interchange::Product; use Interchange::Cart; use Interchange::Class; use Interchange::Query::DBI; use Moo::Role; use Dancer2 qw(!before !after); use Dancer2::Plugin; use Dancer2::Plugin::Database; =head1 NAME Dancer2::Plugin::Interchange - Interchange Shop Machine plugin for Dancer2 =head1 VERSION Version 0.0060 =cut our $VERSION = '0.0060'; =head1 SYNOPSIS use Dancer2::Plugin::Interchange; cart->add({sku => 'ABC', name => 'Foobar', quantity => 1, price => 42}); cart->items(); cart->clear(); account->login(username => 'frank@interchange.com', password => 'nevairbe'); account->acl(check => 'view_prices'); account->logout(); =head1 DESCRIPTION This dancer plugin gives you access to the account and cart functions of the Interchange shop machine. =head1 CARTS The cart keyword returns a L object with the corresponding methods. You can use multiple carts like that: cart('wishlist')->add({sku => 'ABC', name => 'Foobar', quantity => 1, price => 42}); cart('wishlist')->total; The DBI backend (L) allows you to load carts of arbitrary users. cart('', 123)->items; =head1 ACCOUNTS The account keyword returns a L object with the corresponding methods. Login to an account: account->login(username => 'frank@interchange.com', password => 'nevairbe'); Logout: account->logout(); Check permissions: account->acl(check => 'view_prices'); Change password for current account: account->password('nevairbe'); Change password for other account: account->password(username => 'frank@interchange.com', password => 'nevairbe'); Create account: account->create(email => 'fina@interchange.com'); =head1 HOOKS This plugin installs the following hooks: =head2 Add to cart The functions registered for these hooks receive the cart object and the item to be added as parameters. =over 4 =item before_cart_add_validate Triggered before item is validated for adding to the cart. =item before_cart_add Triggered before item is added to the cart. =item after_cart_add Triggered after item is added to the cart. Used by DBI backend to save item to the database. =back =head2 Update cart The functions registered for these hooks receive the cart object, the current item in the cart and the updated item. =over 4 =item before_cart_update Triggered before cart item is updated (changing quantity). =item after_cart_update Triggered after cart item is updated (changing quantity). Used by DBI backend to update item to the database. =back =head2 Remove from cart The functions registered for these hooks receive the cart object and the item to be added as parameters. =over 4 =item before_cart_remove_validate Triggered before item is validated for removal. Receives cart object and item SKU. =item before_cart_remove Triggered before item is removed from the cart. Receives cart object and item. =item after_cart_remove Triggered after item is removed from the cart. Used by DBI backend to delete item from the database. Receives cart object and item. =back =head2 Clear cart =over 4 =item before_cart_clear Triggered before cart is cleared. =item after_cart_clear Triggered after cart is cleared. =back =head2 Rename cart The functions registered for these hooks receive the cart object, the old name and the new name. =over 4 =item before_cart_rename Triggered before cart is renamed. =item after_cart_rename Triggered after cart is renamed. =back =head1 CONFIGURATION The default configuration is as follows: plugins: Interchange: Account: Session: Key: account Provider: DBI Cart: Backend: Session Product: backend: DBI table: products key: sku =head2 ACCOUNT =head3 Connection The connection used by L can be set as follows: plugins: Interchange: Account: Provider: DBI Connection: shop =head3 Fields Extra fields can be retrieved from the account provider and put into the session after a successful login: plugins: Interchange: Account: Provider: DBI Fields: first_name,last_name,city =cut register_hook(qw/before_cart_add_validate before_cart_add after_cart_add before_cart_update after_cart_update before_cart_remove_validate before_cart_remove after_cart_remove before_cart_rename after_cart_rename before_cart_clear after_cart_clear /); my $settings = undef; my %acct_providers; my %carts; hook 'after' => sub { my $carts; my $dsl = shift; # save all carts $carts = $dsl->context->vars->{'interchange_carts'} || {}; for (keys %$carts) { $carts->{$_}->save(); } }; register account => \&_account; sub _account { my $acct; my $dsl = shift; unless ($dsl->context->vars->{'interchange_account'}) { # not yet used in this request $acct = Interchange::Account::Manager->new(provider_sub => \&_load_account_providers); $acct->init_from_session; var interchange_account => $acct; } return $dsl->context->vars->{'interchange_account'}; }; sub _api_object { my (%args) = @_; my $name = shift || 'main'; my $id = shift; my $dsl = shift; for my $thing (%args) { print $thing; } if (defined $id) { $token = "$name\0$id"; } else { $token = $name; } unless (exists $dsl->context->vars->{interchange_carts}->{$token}) { # instantiate cart $dsl->context->vars->{interchange_carts}->{$token} = _create_cart($name, $id); } return $dsl->context->vars->{'interchange_carts'}->{$token}; }; register query => sub { my ($name, $arg, $q, $dbh); my $dsl = shift; if (@_) { $name = shift; $arg = $name; } else { $name = ''; $arg = undef; } unless (exists $dsl->context->vars->{'interchange_query'}->{$name}) { # not yet used in this request unless ($dbh = database($arg)) { die "No database handle for database '$name'"; } $q = Interchange::Query::DBI->new(dbh => $dbh); $dsl->context->vars->{'interchange_query'}->{$name} = $q; } return $dsl->context->vars->{interchange_query}->{$name}; }; sub _update_session { my $dsl; my ($function, $acct) = @_; my ($key, $sref); my $settings = plugin_setting(); # determine session key $key = $settings->{Account}->{Session}->{Key} || 'user'; $function ||= ''; if ($function eq 'init') { # initialize user related information $dsl->app->session{$key} = $acct; } elsif ($function eq 'update') { # update user related information (retrieve current state first) $sref = $dsl->app->session{$key}; for my $name (keys %$acct) { $sref->{$name} = $acct->{$name}; } $dsl->app->session->{$key} = $sref; return $sref; } elsif ($function eq 'destroy') { # destroy user related information $dsl->app->session->{$key} = undef; } else { # return user related information return $dsl->app->session($key); } }; register_plugin for_versions => [2]; sub _load_settings { $settings ||= plugin_setting; } sub _load_account_providers { _load_settings(); # setup account providers if (exists $settings->{Account}->{Provider}) { if ($settings->{Account}->{Provider} eq 'DBI') { return [['Interchange::Account::Provider::DBI', dbh => database($settings->{Account}->{Connection}), fields => _config_to_array($settings->{Account}->{Fields}), inactive => $settings->{Account}->{inactive}, ]]; } else { my $provider_class = $settings->{Account}->{Provider}; unless ($provider_class =~ /::/) { $provider_class = "Interchange::Account::Provider::$provider_class"; } my %account_init = %{$settings->{Account}}; delete $account_init{Provider}; return [[$provider_class, %account_init]]; } } # DBI provider is the default return [['Interchange::Account::Provider::DBI', dbh => database]]; } sub _config_to_array { my $config = shift; my @values; if (defined $config) { @values = split(/\s*,\s*/, $config); return \@values; } return []; } sub _create_cart { my ($name, $id) = @_; my ($backend, $backend_class, $cart, $cart_settings); _load_settings(); if (exists $settings->{Cart}->{Backend}) { $backend = $settings->{Cart}->{Backend}; } else { $backend = 'Session'; } # check for specific settings for this cart name if (exists $settings->{Cart}->{Carts}) { my $sref = $settings->{Cart}->{Carts}; if (ref($sref) eq 'ARRAY') { # walk through settings for my $try (@$sref) { if (exists $try->{name} && $name eq $try->{name}) { $cart_settings = $try; last; } if (exists $try->{match}) { my $match = qr/$try->{match}/; if ($name =~ /$match/) { $cart_settings = $try; last; } } } } elsif (ref($sref) eq 'HASH') { if (exists $settings->{Cart}->{Carts}->{$name}) { $cart_settings = $settings->{Cart}->{Carts}->{$name}; } } else { die "Invalid cart settings."; } } # determine backend class name if ($backend =~ /::/) { $backend_class = $backend; } else { $backend_class = __PACKAGE__ . "::Cart::$backend"; } $cart = Interchange::Class->instantiate($backend_class, name => $name, settings => $cart_settings, run_hooks => sub {execute_hook(@_)}); $cart->load(uid => $id || _account()->uid); return $cart; } =head1 CAVEATS Please anticipate API changes in this early state of development. =head1 AUTHOR Stefan Hornburg (Racke), C =head1 BUGS Please report any bugs or feature requests to C, or through the web interface at L. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes. =head1 SUPPORT You can find documentation for this module with the perldoc command. perldoc Dancer2-Plugin-Interchange You can also look for information at: =over 4 =item * RT: CPAN's request tracker (report bugs here) L =item * AnnoCPAN: Annotated CPAN documentation L =item * CPAN Ratings L =item * Search CPAN L =back =head1 ACKNOWLEDGEMENTS The L developers and community for their great application framework and for their quick and competent support. =head1 LICENSE AND COPYRIGHT Copyright 2010-2012 Stefan Hornburg (Racke). This program is free software; you can redistribute it and/or modify it under the terms of either: the GNU General Public License as published by the Free Software Foundation; or the Artistic License. See http://dev.perl.org/licenses/ for more information. =head1 SEE ALSO L =cut 1;