The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

=head1 NAME
HTML::Tested::ClassDBI - Enhances HTML::Tested to work with Class::DBI
=head1 SYNOPSIS
package MyClass;
use base 'HTML::Tested::ClassDBI';
__PACKAGE__->ht_add_widget('HTML::Tested::Value'
, id => cdbi_bind => "Primary");
__PACKAGE__->ht_add_widget('HTML::Tested::Value'
, x => cdbi_bind => "");
__PACKAGE__->ht_add_widget('HTML::Tested::Value::Upload'
, x => cdbi_upload => "largeobjectoid");
__PACKAGE__->bind_to_class_dbi('MyClassDBI');
# And later somewhere ...
# Query and load underlying Class::DBI:
my $list = MyClass->query_class_dbi(search => x => 15);
# or sync it to the database:
$obj->cdbi_create_or_update;
=head1 DESCRIPTION
This class provides mapping between Class::DBI and HTML::Tested objects.
It inherits from HTML::Tested. Widgets created with C<ht_add_widget> can have
additional C<cdbi_bind> property.
After calling C<bind_to_class_dbi> you would be able to automatically
synchronize between HTML::Tested::ClassDBI instance and underlying Class::DBI.
=cut
use strict;
use warnings FATAL => 'all';
use Carp;
my @_cdata = qw(_CDBI_Class _PrimaryFields _Field_Handlers _PrimaryKey);
__PACKAGE__->mk_classdata($_) for @_cdata;
our $VERSION = '0.21';
sub class_dbi_object { shift()->class_dbi_object_gr('_CDBIM_', @_); }
sub class_dbi_object_gr {
my ($self, $gr, $val) = @_;
return $self->{_class_dbi_objects}->{$gr} if @_ == 2;
$self->{_class_dbi_objects}->{$gr} = $val;
}
sub cdbi_bind_from_fields {
my ($class, $gr) = @_;
for my $v (@{ $class->Widgets_List }) {
my $wgr = $v->options->{cdbi_group} || '_CDBIM_';
$v->options->{cdbi_group} = $wgr;
next unless $wgr eq $gr;
my $f = HTML::Tested::ClassDBI::Field->new($class, $v, $gr)
or next;
$class->_Field_Handlers->{ $v->options->{cdbi_group} }
->{$v->name} = $f;
}
}
sub CDBI_Class { return shift()->_CDBI_Class->{_CDBIM_} }
sub PrimaryFields { return shift()->_PrimaryFields->{_CDBIM_} }
sub Field_Handlers { return shift()->_Field_Handlers->{_CDBIM_} }
sub PrimaryKey { return shift()->_PrimaryKey->{_CDBIM_} }
=head1 METHODS
=head2 $class->bind_to_class_dbi($cdbi_class)
Binds $class to $cdbi_class, by going over all fields declared with C<cdbi_bind>
or C<cdbi_upload> option.
C<cdbi_bind> option value could be one of the following:
name of the column, empty string for the column named the same as field or for
array of columns.
C<cdbi_upload> can be used to upload file into the database. Uploaded file is
stored as PostgreSQL's large object. Its OID is assigned to the bound column.
C<cdbi_upload_with_mime> uploads the file and prepends its mime type as a
header. Use HTML::Tested::ClassDBI::Upload->strip_mime_header to pull it from
the data.
C<cdbi_readonly> boolean option can be used to make its widget readonly thus
skipping its value during update. Read only widgets will not be validated.
C<cdbi_primary> boolean option is used to make an unique column behave as
primary key. C<cdbi_load> will use this field while retrieving the object from
the database.
=cut
sub bind_to_class_dbi { shift()->bind_to_class_dbi_gr('_CDBIM_', @_); }
=head2 $class->bind_to_class_dbi_gr($group, $cdbi_class)
Binds $class to $cdbi class in group $group. Special group _CDBIM_ is used
as the default group.
=cut
sub bind_to_class_dbi_gr {
my ($class, $gr, $dbi_class, $opts) = @_;
$class->$_({}) for grep { !$class->$_ } @_cdata;
$class->_CDBI_Class->{$gr} = $dbi_class;
$class->_Field_Handlers->{$gr} = {};
$class->_PrimaryFields->{$gr} = {};
$class->cdbi_bind_from_fields($gr);
$class->_load_db_info($gr);
my $pk = $opts ? $opts->{PrimaryKey} : undef;
$class->_PrimaryKey->{$gr} = $pk if $pk;
confess "# No Primary fields given\n"
unless ($pk || %{ $class->_PrimaryFields->{$gr} });
}
sub _get_cdbi_pk_for_retrieve {
my ($self, $gr) = @_;
my $pk = $self->_PrimaryKey->{$gr} or goto PFIELDS;
my %pkh;
for my $f (@$pk) {
my $v = $self->$f;
goto PFIELDS unless defined($v);
my $h = $self->_Field_Handlers->{$gr}->{$f};
$pkh{ $h ? $h->column_name : $f } = $v;
}
return \%pkh if %pkh;
PFIELDS:
my $res = {};
my %pf = %{ $self->_PrimaryFields->{$gr} };
my ($pv, $pc);
while (my ($k, $v) = each %pf) {
$pv = $self->$k;
next unless defined $pv;
$pc = $v;
last;
}
return undef unless defined($pv);
my @vals = split('_', $pv);
for (my $i = 0; $i < @$pc; $i++) {
$res->{ $pc->[$i] } = $vals[$i];
}
return $res;
}
sub _fill_in_from_class_dbi {
my ($self, $gr) = @_;
my $fhs = $self->_Field_Handlers->{$gr};
my $cdbi = $self->class_dbi_object_gr($gr);
while (my ($f, $h) = each %$fhs) {
$self->$f($h->get_column_value($cdbi));
}
}
sub cdbi_retrieve { shift()->_call_for_all('cdbi_retrieve_gr', @_); }
sub cdbi_retrieve_gr {
my ($self, $gr) = @_;
my $pk = $self->_get_cdbi_pk_for_retrieve($gr);
return unless defined($pk);
my $cdbi = $self->_CDBI_Class->{$gr}->retrieve(ref($pk) ? %$pk : $pk);
$self->class_dbi_object_gr($gr, $cdbi);
return $cdbi;
}
=head2 $obj->cdbi_load
Loads Class::DBI object using primary key field - the widget with special
C<cdbi_bind> => 'Primary'.
This method populates the rest of the bound fields with values of the loaded
Class::DBI object.
Returns retrieved Class::DBI object or undef.
=cut
sub cdbi_load { return shift()->_call_for_all('cdbi_load_gr', @_); }
sub _get_cdbi_object {
my ($self, $gr) = @_;
return $self->class_dbi_object_gr($gr) || $self->cdbi_retrieve_gr($gr);
}
sub cdbi_load_gr {
my ($self, $gr) = @_;
my $cdbi = $self->_get_cdbi_object($gr) or return;
$self->_fill_in_from_class_dbi($gr);
return $cdbi;
}
=head2 $class->query_class_dbi($func, @params)
This function loads underlying Class::DBI objects using query function $func
(e.g C<search>) with parameters contained in C<@params>.
For each of those objects new HTML::Tested::ClassDBI instance is created.
=cut
sub query_class_dbi {
my ($class, $func, @params) = @_;
my @cdbis = $class->CDBI_Class->$func(@params);
return [ map {
my $c = $class->new;
$c->class_dbi_object($_);
$c->_fill_in_from_class_dbi('_CDBIM_');
$c;
} @cdbis ];
}
sub _call_for_all {
my ($self, $func, @args) = @_;
$self->$func($_, @args) for keys %{ $self->_CDBI_Class };
return $self->class_dbi_object;
}
=head2 $obj->cdbi_create($args)
Creates new database record using $obj fields.
Additional (optional) arguments are given by $args hash refernce.
=cut
sub cdbi_create { return shift()->_call_for_all('cdbi_create_gr', @_); }
=head2 $class->cdbi_create_gr($group, $args)
Creates new database record using $obj fields in group $group.
=cut
sub cdbi_create_gr {
my ($self, $gr, $args) = @_;
my $cargs = $self->_get_cdbi_pk_for_retrieve($gr) || {};
$self->_update_fields($gr, sub { $cargs->{ $_[0] } = $_[1]; }, $args);
my $res;
eval { $res = $self->_CDBI_Class->{$gr}->create($cargs); };
confess "SQL error: $@\n" . Dumper($self) if $@;
$self->class_dbi_object_gr($gr, $res);
$self->_fill_in_from_class_dbi($gr);
return $res;
}
sub _update_fields {
my ($self, $gr, $setter, $args) = @_;
while (my ($field, $h) = each %{ $self->_Field_Handlers->{$gr} }) {
$h->update_column($setter, $self, $field)
if exists $self->{$field};
}
my $cdbi = $self->_CDBI_Class->{$gr};
while (my ($n, $v) = each %{ $args || {} }) {
$setter->($n, $v) if $cdbi->can($n);
}
}
=head2 $obj->cdbi_update($args)
Updates database records using $obj fields.
Additional (optional) arguments are given by $args hash refernce.
=cut
sub cdbi_update { return shift()->_call_for_all('cdbi_update_gr', @_); }
sub cdbi_update_gr {
my ($self, $gr, $args) = @_;
my $cdbi = $self->_get_cdbi_object($gr)
or confess("# Nothing found to update");
$self->_update_fields($gr, sub {
my ($c, $val) = @_;
no warnings 'uninitialized';
$cdbi->$c($val) if $cdbi->$c ne $val;
}, $args);
eval { $cdbi->update; };
confess "SQL error: $@\n" . Dumper($self) if $@;
$self->_fill_in_from_class_dbi($gr);
return $cdbi;
}
=head2 $obj->cdbi_create_or_update($args)
Calls C<cdbi_create> or C<cdbi_update> based on whether the database record
exists already.
Additional (optional) arguments are given by $args hash refernce.
=cut
sub cdbi_create_or_update {
return shift()->_call_for_all('cdbi_create_or_update_gr', @_);
}
sub cdbi_create_or_update_gr {
my ($self, $gr, $args) = @_;
return $self->_get_cdbi_object($gr) ? $self->cdbi_update_gr($gr, $args)
: $self->cdbi_create_gr($gr, $args);
}
=head2 $obj->cdbi_construct
Constructs underlying Class::DBI object using $obj fields.
=cut
sub cdbi_construct { return shift()->cdbi_construct_gr('_CDBIM_'); }
sub cdbi_construct_gr {
my ($self, $gr) = @_;
my $pk = $self->_get_cdbi_pk_for_retrieve($gr)
or confess "No primary key for $gr";
return $self->_CDBI_Class->{$gr}->construct($pk);
}
=head2 $obj->cdbi_delete
Deletes database record using $obj fields.
=cut
sub cdbi_delete { shift()->cdbi_delete_gr('_CDBIM_', @_); }
sub cdbi_delete_gr {
my $c = shift()->cdbi_construct_gr(shift());
$c->delete;
}
sub _load_db_info {
my ($class, $gr) = @_;
while (my ($n, $h) = each %{ $class->_Field_Handlers->{$gr} }) {
my $w = $class->ht_find_widget($n);
$h->setup_type_info($class->_CDBI_Class->{$gr}, $w);
}
}
=head2 $class->cdbi_set_many($class_objs, $cdbi_objs)
Initializes C<class_dbi_object> field of C<$class_objs> arrayref from the
Class::DBI objects given in C<$cdbi_objs>.
Useful to avoid overhead of retrieving Class::DBI objects one by one.
=cut
sub cdbi_set_many {
my ($class, $h_objs, $c_objs) = @_;
my @pcs = $class->CDBI_Class->primary_columns;
my %c_objs;
for my $co (@$c_objs) {
$c_objs{ join('_', map { $co->$_ } @pcs) } = $co;
}
for my $ho (@$h_objs) {
my $pk = $ho->_get_cdbi_pk_for_retrieve('_CDBIM_');
my $co = $c_objs{ join('_', grep { defined($_) }
map { $pk->{$_} } @pcs) };
$ho->class_dbi_object($co);
}
}
1;
=head1 AUTHOR
Boris Sukholitko
CPAN ID: BOSU
boriss@gmail.com
=head1 COPYRIGHT
This program is free software; you can redistribute
it and/or modify it under the same terms as Perl itself.
The full text of the license can be found in the
LICENSE file included with this module.
=head1 SEE ALSO
HTML::Tested, Class::DBI
=cut