package DB::Objects;

use strict;
use warnings qw(all);
use vars qw($VERSION);

BEGIN {
    $VERSION=0.03;
}

###

package DB::Object;

use strict;
use warnings qw(all);

# Back-end methods
sub _blank {    # Default back-end for constructor
                # ARGS: $self, [$namespace,] @arglist
                # $namespace - Scalar (string) - Namespace of managing package for variables
                # @arglist   - Array (string) - List of variables to be registered as methods
    my $self=shift;
    my $package=
          (UNIVERSAL::isa($_[0],__PACKAGE__) && # Looks like a descendant
          shift) || caller; # Shift or autodetect namespace to register
    warn "Package $package not listed in registry"
        unless defined($self->{_REGISTRY}{$package});
    while (@_) {
        local $_=uc(shift);
        $self->{$_}=undef;
        $self->{_REGISTRY}{_DATA}{$_}{source}=$package;
        $self->{_REGISTRY}{_DATA}{$_}{access}=1; # default to rw
	$self->{_REGISTRY}{_DATA}{$_}{type}="basic";
    }
}

sub _register { # Default back-end for package registration
                # Call immediately after being bless()ed
    my $self=shift;
    my $package=caller;
    $self->{_REGISTRY}{$package}{prep}=0 unless (defined($self->{_REGISTRY}{$package}));
    return defined($self->{_REGISTRY}{$package});
}

sub _unregister{# Default back-end for package de-registration
                # If you wish to partially destruct an object, make sure to call this
                # from each namespace being removed from the object
    my $self=shift;
    my $package=caller;
    $self->_taint($package);
    undef $self->{_REGISTRY}{$package};
    return (!(defined($self->{_REGISTRY}{$package})));
}


sub _primary {  # Sets/detects whether a namespace contains the primary key
                # Used internally to assure that the primary key's namespace is always
                # in sync with the rest of the object
    my $self=shift;
    my $package=(UNIVERSAL::isa($_[0],__PACKAGE__) && shift) || caller;
    if ($_[0]) {$self->{_REGISTRY}{_PRIMARY}=$package;$self->_taint;}
    return ($self->{_REGISTRY}{_PRIMARY} || 0);
}

sub _readonly { # Sets/detects whether a data mehod is tagged read-only
                # Used by AUTOLOAD to detect read-only method calls
    my $self=shift;
    my $package=(UNIVERSAL::isa($_[0],__PACKAGE__) && shift) || caller;
    my $var=uc(shift);
    if (@_) {local $_=shift;$self->{_REGISTRY}{_DATA}{$var}{access}=(!($_)?1:0) if (/[01]/);} #Set to "0" to catch in this check next time
    return (!($self->{_REGISTRY}{_DATA}{$var}{access}) ||
            $self->{_REGISTRY}{_DATA}{$var}{source} eq $self->_primary);
}

sub _validate { # Marks a namespace as tied to the back-end database
                # Intended to be called on first refresh - Paired with _taint
    my $self=shift;
    my $package=shift || caller;
    (my @vars=$self->_vars($package)) || return $self;
    foreach my $var (@vars) {
	if ($self->_isobject($var)) { # Reset embedded object information (only if needed)
	    unless ($self->{var} && ($self->{_REGISTRY}{_DATA}{$var}{data} eq $self->{$var})) {
		$self->{_REGISTRY}{_DATA}{$var}{data}=$self->{$var};
		$self->{_REGISTRY}{_DATA}{$var}{prep}=0;
		$self->{$var}=undef;
	    }
	}
    }
    $self->{_REGISTRY}{$package}{prep}=1;
    $self->{_REGISTRY}{ref($self)}{prep}=1;
    $self->_clean($package);
}

sub _taint {    # Marks a namespace as untied from the back-end database
                # Intended to be called on destruction only
    my $self=shift;
    my $package=shift || caller;
    $self->_dirty($package);
    (my @vars=$self->_vars($package)) || return $self;
    foreach my $var (@vars) {
	if ($self->_isobject($var)) { # Reset embedded object information (only if needed)
	    $self->{_REGISTRY}{_DATA}{$var}{prep}=0;
	    $self->{$var}=undef;
	}
    }
    $self->{_REGISTRY}{$package}{prep}=0;
}

sub _clean {    # Marks a namespace as in-sync with the back-end database
                # Intended to be called on all calls to add(), refresh() and update()
    my $self=shift;
    my $package=shift || caller;
    $self->{_REGISTRY}{$package}{dirty}=0;
}

sub _dirty {    # Marks a namespace as out-of-sync with the back-end databse
                # Intended to be called upon a write-access call to a class-method
    my $self=shift;
    my $package=shift || caller;
    $self->{_REGISTRY}{$package}{dirty}=1;
}

sub _vars {     # Returns a list of variables registered to a specific namespace
                # Used internally by default _refresh() and update() methods
    my $self=shift;
    my $package=shift || caller;
    my @vars = ();
    my @keys = keys(%{$self->{_REGISTRY}{_DATA}});
    foreach my $var(@keys) {
        push @vars,$var if ($self->{_REGISTRY}{_DATA}{$var}{source} eq $package);
    }
    return @vars;
}

sub _refresh {  # Default back-end for refresh
                # Inherited classes should implement a custom _refresh()
                # Alternatively, the default _refresh may be used if a valid DBI connection
                # is set using $__PACKAGE__::dbh and the table is set to $__PACKAGE__::table
    my $self=shift;
    my $package=(UNIVERSAL::isa($_[0],__PACKAGE__) && shift) || ref($self);
    my @vars=$self->_vars($package) || return $self;
    my $sth;
    {
        no strict 'vars';
        eval "\$sth=\$dbh->prepare_cached('SELECT \@vars FROM \$table WHERE (ID=?)');";
    }
    $sth->execute(@_) or return $self->blank;
    if ($sth->rows!=1) {
	$self->blank;
    } else {
	my $res=$sth->fetchrow_hashref;
	foreach my $var (@vars) {
	    $self->{$var}=$res->{$var};
	}
        $self->_validate;
    }
    $sth->finish;
    return $self;
}

sub AUTOLOAD {  # Default method call handler
                # Current support:
                #    * Read/Write registered methods from internal hash
    my $param;
    my $package;
    {
        no strict 'vars';
        $AUTOLOAD=~s/(.*):://;
	$package=$1;
        $param=$AUTOLOAD;
    }
    if (UNIVERSAL::isa($_[0],__PACKAGE__)) { # Method call of a sub-class
        my $self=shift;
        if ($self->{_REGISTRY}{_DATA}{uc($param)}) { # Acceptable function call
            my $source=$self->{_REGISTRY}{_DATA}{uc($param)}{source};
            if (!($self->valid($source))) {
                $self->refresh($source);
            }
	    # SET access
            if ((@_) && !($self->_readonly($source,$param))) { # Update rewriteable request
		if ($self->_isbasic($param)) {
		    $self->{uc($param)}=@_;
		    $self->_taint;
		} elsif ($self->_isobject($param)) { # Object SET
		    unless ($self->_isobjarray($param)) { # No SET allowed on arrays
			my ($temp,$pid)=@_;
			if (ref($temp) eq $self->{_REGISTRY}{_DATA}{uc($param)}{class}) {
			    # TODO: see if temp->isa($self->{_REGISTRY}{_DATA}{uc($param)}{class})
			    $pid=$temp->id; #Retrieve ID from internal object
			} else {
			    $pid=$temp; #Assume ID is specified if not compatible object
			}
			$self->{_REGISTRY}{_DATA}{uc($param)}{data}=$pid;
			$self->_taint;
		    }
		}
            } # GET access
	    if ($self->_isobject($param)) { # Prepare object
		return (wantarray?undef:0) unless $self->_o_prep($param);
	    }
	    if ($self->_isobjarray($param)) { # Object array returns special values
		    return (wantarray?@{$self->{uc($param)}}:$self->{_REGISTRY}{_DATA}{uc($param)}{prep});
	    } else {
		return $self->{uc($param)};
	    }
        }
    }
}

sub new {       # Default constructor
                # Do not overload this unless you're SURE you know what you're doing
    my $self={ };
    my $proto=shift;
    my $class=ref($proto) || $proto;
    bless $self,$class;
    eval "foreach \$_ (\@".$class."::ISA) {eval \$_.\"::blank(\\\$self);\";}";
    $self->_register;
    $self->blank(@_);
    if (@_) {
        eval ($self->_primary."::_refresh(\$self,'".$self->_primary."',@_);") if ($self->_primary);
	$self->_refresh(@_);
    }
    return $self;
}

sub clean {     # Returns true if namepace is in-sync with back-end database
                # Be sure to check for valid()ity BEFORE using this
    my $self=shift;
    my $package=shift || caller;
    return !($self->{_REGISTRY}{$package}{dirty});
}

sub valid {     # Returns true if namespace is tied and in-sync with back-end database
    my $self=shift;
    my $package=shift || ref($self);
    if ($self->_primary)
    {return ($self->{_REGISTRY}{$self->_primary}{prep} &&
             $self->clean($self->_primary) &&
             $self->{_REGISTRY}{$package}{prep} &&
             $self->clean($package))}
    else {return $self->{_REGISTRY}{$package}{prep} &&
                 $self->clean($package)};
}

sub blank {     # Default (abstract) blank method - used by the default constructor
                # This should be overridden by any inherited class that's meant to be useful
                # A typical blank() method should look like:
                #    sub blank {
                #        my $self=shift;
                #        $self->_register;
                #        $self->_blank("FOO", "BAR", ... , "LAST");
                #    }
    $_[0]->_register;
}

sub refresh {   # Default front-end for refresh
    my $self=shift;
    my $package=shift || ref($self);
    $self->_taint($package);
    eval $package."::_refresh(\$self,".$self->id.");";
    return $self->valid;
}

sub _isbasic { # Returns true if access method marked as basic (default)
    my $self=shift;
    my $var=uc(shift);
    return ($self->{_REGISTRY}{_DATA}{$var}{type} eq "basic");
}

sub _isobject { # Returns true if access method marked as embedded object
    my $self=shift;
    my $var=uc(shift);
    return ($self->{_REGISTRY}{_DATA}{$var}{type} eq "object");
}

sub _isobjarray {
    my $self=shift;
    my $var=uc(shift);
    return ($self->_isobject($var) && $self->{_REGISTRY}{_DATA}{$var}{array});
}

sub _object { # Marks an access member as an object (call in blank)
    my $self=shift;
    my $var=uc(shift);
    my $package=(UNIVERSAL::isa($_[0],__PACKAGE__) && shift) || ref($self);
    $self->{_REGISTRY}{_DATA}{$var}{prep}=0;
    $self->{_REGISTRY}{_DATA}{$var}{class}=$package;
    $self->{_REGISTRY}{_DATA}{$var}{data}=$self->{$var} || undef;
    $self->{_REGISTRY}{_DATA}{$var}{array}=0;
    $self->{_REGISTRY}{_DATA}{$var}{type}="object";
    $self->{$var}=undef;
}

sub _objarray { # Marks an access member as an array of objects (call in blank)
    my $self=shift;
    my $var=uc(shift);
    my $package=(UNIVERSAL::isa($_[0],__PACKAGE__) && shift) || ref($self);
    $self->_object($var,$package);
    $self->_readonly($var,1);
    $self->{_REGISTRY}{_DATA}{$var}{array}=1;
}

# This (objarray) won't be fully implemented until I can figure out how the heck
# to set the data source as an array - it probably has to be dealt with by
# end-module's refresh ($self->{$var}=@arrayofdata;) [UPDATE VALIDATE TO DEAL WITH THIS]...

sub _o_prep {
    my $self=shift;
    my $var=uc(shift);
    my $class=$self->{_REGISTRY}{_DATA}{$var}{class};
    my $source=$self->{_REGISTRY}{_DATA}{$var}{source};
    return 0 unless $self->valid($source);
    return $self->{_REGISTRY}{_DATA}{$var}{prep} if
	$self->{_REGISTRY}{_DATA}{$var}{prep};
    if ($self->_isobjarray($var)) {
	for (my $i=0;$i<=$#{$self->{_REGISTRY}{_DATA}{$var}{data}};$i++) {
	    $self->{$var}[$i]=$class->new($self->{_REGISTRY}{_DATA}{$var}{data}[$i]);
	}
	$self->{_REGISTRY}{_DATA}{$var}{prep}=$#{$self->{_REGISTRY}{_DATA}{$var}{data}}+1;
    } else {
	$self->{_REGISTRY}{_DATA}{$var}{prep}=(($self->{$var}=$class->new($self->{_REGISTRY}{_DATA}{$var}{data}))?1:0);
    }
    return $self->{_REGISTRY}{_DATA}{$var}{prep};
}

# TODO: Update updates recursively (into embedded objects)
#        UpdateNR updates non-0recursively (data ghets lost)

# DOCUMENT: _validate also initializes objects by setting internal DATA value and clearing external value

1;

__END__

=head1 NAME

DB::Objects - Perl extension to ease creation of database-bound objects

=head1 SYNOPSIS

This module is intended to provide an object-oriented framework for
accessing data sources.  The source of the data is completely abstract,
allowing for complete flexibility for the data back-end.  This module is
NOT intended to provide a persistance layer - please use another module
(like TANGRAMS) if you require object persistance.

I'm really not sure how to go about documenting this library, so let me
start by explaining the history of why it was written

=head1 BACKGROUND

I developed this module when I began to notice that most of my web-applications
followed a very similar format - there was a data back end and web methods
which could interoperate with them.  When I started to need helper applications
to work with the web-apps, I started porting all of my applications to use 2
layers.  The lower layer was an object framework which contained the Perl
code needed to work with the database.  This way, I could be sure that all of
the helper applications, and of course the web application, all used the same
access methods to get to the database to eleminate the possibility that something
was getting f#$%ed up in the database by a faulty query somewhere in the big mess of code.
(The upper layer was the "business logic" layer, which was the web or helper
application.)

Then, I noticed that all of these database access objects were very similar:
they all had access methods for each member of the class, which represented
a single field in the database and had select/insert/update/delete routines.

I'd also developed a "dynamic object" at this point, where I'd have a huge
variable-length field in the database which conatained many fields.  This
way I could change the object without worrying about compatibility in the
back end database if I added/changed/removed fields.  (We'll get back to
this later.)

Beyond that, there were different ways of embedding objects (for example,
a person object might have a phone number object embedded in it as part
of an address-book application).  (We'll get back to this later, too).

So there were different ways of logically grouping different sets of data,
but the objects all shared a unified way of accessing the data.  Thus was
DB::Objects born - it provided a framework which would reallly guarantee
that the objects would really function in a logically similar way - similar
to the way that most GUI applications work in logically similar ways (they
all have that File menu with Open , Save, Exit... The Help menu with Help
topics, an optional upgrade, etc).  So I guess you could call this library
an API for developing database bound objects.

For more information, see http://www.beamartyr.net/YAPC/

=head1 BASIC OBJECTS API

The most basic type of object that can be used with this library simply
get tied directly to fields in the database.

=item blank($self, @args)

This is your constructor.  Anything that is important for you to add to the
object's new() method should be done here.  DO NOT DECLARE YOUR OWN new()
FUNCTION!  This function gets a bless()ed $self passed as the first parameter,
followed by the argument list to the new() call.  The constructor is expected
to call the internal _blank() function described below, and should call _primary(),
if applicable.

In addition to being called by the constructor, this function is also called every
time an empty (blank) instance of the object is needed.

A sample structure for the blank() method is included for reference:

sub blank {
    my $self=shift;
    $self->_register;
    $self->_blank("FOO", "BAR", ... , "LAST");
    $self->_primary(1); # Optional - Marks as containing primary key
}

=item _refresh($self, [$package], $id)

This is called internally by refresh() and should contain code to sync the data
structure with the back end database.  Note that the $package variable is optional,
but should be checked for (META: Remove code that makes this necessary). The
subroutine should either refresh the data from the database and call _validate() or,
if no suitable data is found, should call blank() [NOT _blank()]. In either case,
it should return $self.  A sample structure for the _refresh() method is included here
for reference:

sub _refresh {
    my $self=shift; # Shift $self
    my $package=(UNIVERSAL::isa($_[0],__PACKAGE__) && shift) || ref($self); # Detect $package
    my $sth=$dbh->prepare_cached('SELECT FIRSTNAME, LASTNAME FROM PEOPLE WHERE (ID=?)'); # Set SQL
    $sth->execute(@_) or return $self->blank; # Run SQL or return blank() object
    if ($sth->rows!=1) { # A good SQL statement should always return EXACTLY one row
	$self->blank; # Bad SQL - return blank()
    } else { # Good SQL
	my $res=$sth->fetchrow_hashref; # Fetch results
        $self->{FIRSTNAME}=$res->{FIRSTNAME}; # Each element from the result set
	$self->{LASTNAME}=$res->{LASTNAME}; # gets passed to exactly one hash element
        $self->_validate; # And call _validate() to mark it as clean and in-sync with the DB
    }
    $sth->finish; # Finished with SQL
    return $self; # ...and return $self
}

=head1 ADVANCED OBJECTS API

Beyond storing simple values, you can also magically construct objects using
the primary key of the target objects.  Using the above example,
we spoke of a phone book catalog, which has a person object and a phone number object.
Would it be nice, if instead of writing:

my $phone_number=Phone_Number->new($person->phone_number);
print "Number is ".$phone_number->thenumber;

... you could simply write ...

print "Number is ".$person->phone_number->thenumber;

The advanced objects API lets you do just that, with almost no extra work on your behalf.

=item _object($var, [$package])

This should be called in the blank() constructor.  It is used to mark access
method $var as an embedded object of type $package.  If $package is not passed
as an argument, it will default to the current package (eg, Your::Object).  To
use the feature, simply store the data you want passed to the object's constructor
the same way you'd store any other data for a normal access method, and when
you _validate the object, the data will be appropriately stored.  The object will be
constructed on the first call to the access method.

=item _objarray($var, [$package])

This should be called in the blank() constructor.  It is used to mark access method
$var as an embedded array of objects of type $package.  If $package is not passed
as an argument, it will default to the current package (eg, Your::Object).  The
functionality is similar to that of _object except that we are dealing with arrays.
As such, care must be taken to properly initialize the array.  To use it, store an
array (not a reference) in the _refresh function.  When _validate is called on the
object, the array will be stored and objects will be called on subsequent access
via the method call $var.  The method call will return the number of objects in
the array if called in a scalar context, and the array of objects if called in a
list context.  The objects will be constructed upon access.

Note that this functionality is still under construction.

=head1 INTERNAL FUNCTIONS

=item _blank(@vars)

This should be called by the blank() constructor.  The arguments should be all of the
access methods provided by this class.  It should *not* include inherited access
methods, as they will automatically be discovered by AUTOLOAD.  This will register
all of the access methods in the registry under the module's namespace, so that
AUTOLOAD can auto-load the module to refresh or update the database for it.

=item _validate()

This method should be called from the _refresh function.  It tells the object that
its data has been updated and to remark itself as having fresh unchanged data.

=head1 AUTHOR AND COPYRIGHT

Copyright (c) 2003 Issac Goldstand E<lt>margol@beamartyr.netE<gt> - All rights reserved.

This library is free software. It can be redistributed and/or modified
under the same terms as Perl itself.

=head1 SEE ALSO

L<perl>.

=cut
