On 1/30/07, Derek Watson <[EMAIL PROTECTED]> wrote:
> I am using RDBO in conjunction with Rose::HTML::Form for the first time, and
> wonder if there are any guides or examples out there showing some
> conventional integration techniques.  Specifically, how best to use RHF's
> objects returned by object_from_form() style methods to update database
> rows.  Currently I have something like this:
>
>   $form->init_with_customer($session->customer);  # is a
> load()ed RDBO object
>   if ($ENV{REQUEST_METHOD} eq 'POST') {
>
>     $form->params(\%ARGS);
>     $form->init_fields();
>
>     if ($form->validate) {
>       my $c = $form->customer_from_form;  # is RDBO::Customer->new()
>       $c->id($session->customer_id);  # avoid an insert
>       $c->save(update => 1, changes_only => 1, cascade => 1); # force an
> update
>     }
>   }
>
> Which works, but seems like there is probably a more conventional way to do
> this.

Well, the details depend on your particular app and on the web
application framework that you're using (if any).  What you have there
looks sensible to me.  One thing I would suggest is that you consider
having the customer_from_form() method do all the stuff required to
set up the object (e.g., setting the id).

In the example above, it's a little tricky because the value for the
id is in $session, which the form knows nothing about.  But in most
web app frameworks, there's usually some way to get at "globally
applicable" data like the session, in which case it's reasonable for
customer_from_form() to return you an object that's completely ready
to be save()d, without any fiddling, and, ideally, without need for an
explicit update => ... argument.

To this end, consider having customer_from_form() internally
load(speculative => 1) the RDBO-derived object based on the id first,
and then pass that object to object_from_form().  This will let you
have forms that contain only a subset of the object attributes, and it
will also ensure that a save() call does the right thing (an update,
in this case).

> But my real problem comes when my form returns other objects as well,
> which are related to customer ($c) . . .for example
>
>     if ($form->validate) {
>       my $c = $form->customer_from_form;  # is RDBO::Customer->new()
>        $c->id($session->customer_id);  # avoid an insert
>
>       my $a = $form->address_from_form;  # is RDBO::Address->new()
>        $c->billing_address($a);
>
>       # inserts a new RDBO::Address record, but unfortunately I want an
>       # update
>       $c->save(update => 1, changes_only => 1, cascade => 1);
>      }
>
> Is there a better way to use these two packages together?

I have db_object_from_form() and init_with_db_object() methods that
will handle a tree of RDBO-derived objects that correspond to a tree
of nested RHTMLO forms.  The code is included at the end of this
email, but it's still "in progress" and may be buggy.  Take from it
what you will.

The basic idea is that, if a Person has a related Address, then the
PersonAddress form is a PersonForm with a nested AddressForm using the
same name as the relationship between Person and Address.  Example:

    package Person;
    ...
    __PACKAGE__->meta->setup
    (
      ...
      relationships =>
      [
        address =>
        {
          type  => 'one to one',
          class => 'Address',
          column_map => { id => 'person_id' },
        },
      ]
    );
    ...


    package PersonForm;

    use base 'Rose::HTML::Form';
    ...
    sub build_form
    {
      my($self) = shift;

      $self->add_fields
      (
        name => { ... },
        age  => { ... },
        ...
      );
    }
    ...


    package PersonAddressForm;

    use base 'PersonForm';
    ...
    sub build_form
    {
      my($self) = shift;

      $self->SUPER::build_form(@_); # Adds Person fields

      # Add a sub-form for Address under the same name as the one-to-one
      # RDBO relationship between Person and Address: "address"
      $self->add_form(address => AddressForm->new);
    }

    sub person_from_form
    {
      my($self) = shift;

      my $person = Person->new;

      if(my $id = $self->field('id')->internal_value)
      {
        $person->id($id);
        $person->load(with => 'address');
      }

      # Returns a Person object complete with an Address sub-object
      return $self->db_object_from_form($person);
    }

    sub init_with_person
    {
      my($self, $person) = @_;
      $self->init_with_db_object($person);
    }

Then, in action:

    $form = PersonAddressForm->new;

    ...init form with params, validate, etc....

    # The $person object returned has an address sub-object
    $person = $form->person_from_form;

    # Save the Person and its Address in a single transaction
    $person->save;

The code for db_object_from_form() and init_with_db_object() is below.
 Remember, it may be buggy and/or unsuitable for your purposes.  It's
just something I've been playing with.  If/when I'm happy with it,
it'll find its way into Rose::HTML::Form, or perhaps
Rose::HTML::Form::RDBO or something.

-John

---

package My::Form;

use strict;

use base 'Rose::HTML::Form';

use Rose::DB::Object::Util qw(has_loaded_related);
use Rose::HTML::Form::Constants qw(FF_SEPARATOR);

# Variables for use in regexes
our $FF_SEPARATOR_RE = quotemeta FF_SEPARATOR;
our $FF_SEPARATOR    = FF_SEPARATOR;

our $Debug = 0;

sub db_object_from_form
{
  my($self) = shift;

  my($class, $object);

  if(@_ == 1)
  {
    $class = shift;

    if(ref $class)
    {
      $object = $class;
      $class = ref $object;
    }
  }
  elsif(@_)
  {
    my %args = @_;

    $class  = $args{'class'};
    $object = $args{'object'};
  }
  else
  {
    croak "Missing required object class argument";
  }

  $object ||= $class->new();

  my $parent_object = $object;

  unless($object->isa('Rose::DB::Object'))
  {
    croak "$object is not a Rose::DB::Object-derived object";
  }

  my $meta = $object->meta;

  foreach my $field ($self->fields)
  {
    my $name = $field->name;

    next  unless($self->param_exists_for_field($name));

    $object = $parent_object;

    my $partial_name = '';

    if($name =~ /$FF_SEPARATOR_RE/o)
    {
      my $nibble = $name;
      my $obj    = $object;

      while($nibble =~ s/^([^$FF_SEPARATOR]+)(?:$FF_SEPARATOR_RE)//o)
      {
        my $related = $1;

        last  unless($obj->can($related));

        if(has_loaded_related($obj, $related))
        {
          $obj = $obj->$related()
        }
        else
        {
          my $new_obj;
          eval { $new_obj = $obj->$related() };

          if($@ || !$new_obj)
          {
            # Restore failed segment
            $nibble = "$related$FF_SEPARATOR$nibble";
            last;
          }

          $obj = $new_obj;
        }
      }

      if($nibble =~ /$FF_SEPARATOR_RE/o)
      {
        $name = $field->local_name;
      }
      else
      {
        $name   = $nibble;
        $object = $obj;
      }
    }
    else
    {
      $name = $field->local_name;
    }

    if($object->can($name))
    {
      # Checkboxes setting boolean columns
      if($field->isa('Rose::HTML::Form::Field::Checkbox') &&
         $meta->column($name) && $meta->column($name)->type eq 'boolean')
      {
        #$Debug && warn "$object->$name(", $field->is_on, ")\n";
        $object->$name($field->is_on);
      }
      else # everything else
      {
        my $value = $field->internal_value;
        $value = undef  unless(length $value);

        #$Debug && warn "$object->$name($value)\n";
        eval { $object->$name($value) };

        if($@)
        {
          $self->error("Could not set $object->$name($value) - $@");
          warn $self->error;
          return undef;
        }
      }
    }
  }

  return $parent_object;
}

sub init_with_db_object
{
  my($self, $object) = @_;

  croak "Missing required object argument"  unless($object);

  $self->clear();

  foreach my $field (sort { $a->name cmp $b->name } $self->fields)
  {
    my $name = $field->name;

    if($name =~ /$FF_SEPARATOR_RE/o)
    {
      my $nibble = $name;
      my $obj    = $object;

      while($nibble =~ s/^([^$FF_SEPARATOR]+)$FF_SEPARATOR_RE//o)
      {
        my $related = $1;
        last  unless($obj->can($related));

        if(has_loaded_related($obj, $related))
        {
          $obj = $obj->$related()
        }
        else
        {
          my $new_obj;
          eval { $new_obj = $obj->$related() };

          if($@ || !$new_obj)
          {
            # Restore failed segment
            $nibble = "$related$FF_SEPARATOR$nibble";
            last;
          }

          $obj = $new_obj;
        }
      }

      if($nibble =~ /$FF_SEPARATOR_RE/o)
      {
        $name = $field->local_name;
      }
      else
      {
        $name   = $nibble;
        $object = $obj;
      }
    }
    else
    {
      $name = $field->local_name;
    }

    if($object->can($name))
    {
      #$Debug && warn field($name) = $object->$name = ", $object->$name(), "\n";
      $field->input_value(scalar $object->$name());
    }
  }
}

1;

-------------------------------------------------------------------------
Take Surveys. Earn Cash. Influence the Future of IT
Join SourceForge.net's Techsay panel and you'll get the chance to share your
opinions on IT & business topics through brief surveys - and earn cash
http://www.techsay.com/default.php?page=join.php&p=sourceforge&CID=DEVDEV
_______________________________________________
Rose-db-object mailing list
Rose-db-object@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/rose-db-object

Reply via email to