DBIx::Class::FormTools - Helper module for building forms with multiple related L<DBIx::Class> objects.


DBIx-Class-FormTools documentation Contained in the DBIx-Class-FormTools distribution.

Index


Code Index:

NAME

Top

DBIx::Class::FormTools - Helper module for building forms with multiple related DBIx::Class objects.

VERSION

Top

This document describes DBIx::Class::FormTools version 0.0.5

SYNOPSIS

Top

This is ALPHA software

There may be bugs. The interface may change. Do not use this for anything important just yet.

Prerequisites

In the examples I use 3 objects, a Film, an Actor and a Role. Role is a many to many relation between Film and Actor.

    package MySchema;
    use base 'DBIx::Class::Schema';
    __PACKAGE__->load_classes(qw[
        Film
        Actor
        Role
    ]);




    package MySchema::Film;
    __PACKAGE__->table('films');
    __PACKAGE__->add_columns(qw[
        id
        title
    ]);
    __PACKAGE__->set_primary_key('id');
    __PACKAGE__->has_many(roles => 'MySchema::Role', 'film_id');




    package MySchema::Actor;
    __PACKAGE__->table('films');
    __PACKAGE__->add_columns(qw[
        id
        name
    ]);
    __PACKAGE__->set_primary_key('id');
    __PACKAGE__->has_many(roles => 'MySchema::Role', 'actor_id');




    package MySchema::Role;
    __PACKAGE__->table('roles');
    __PACKAGE__->add_columns(qw[
        film_id
        actor_id
    ]);
    __PACKAGE__->set_primary_key(qw[
        film_id
        actor_id
    ]);

    __PACKAGE__->belongs_to(film_id  => 'MySchema::Film');
    __PACKAGE__->belongs_to(actor_id => 'MySchema::Actor');




In your Controller

    use DBIx::Class::FormTools;

    my $helper = DBIx::Class::FormTools->({ schema => $schema });




In your view - HTML::Mason example

    <%init>
    my $film  = $schema->resultset('Film')->find(42);
    my $actor = $schema->resultset('Actor')->find(24);
    my $role  = $schema->resultset('Role')->new;

    </%init>
    <form>
        <input
            name="<% $helper->fieldname($film, 'title', 'o1') %>"
            type="text"
            value="<% $film->title %>"
        />
        <input
            name="<% $helper->fieldname($film, 'length', 'o1') %>"
            type="text"
            value="<% $film->length %>"
        />
        <input
            name="<% $helper->fieldname($film, 'comment', 'o1') %>"
            type="text"
            value="<% $film->comment %>"
        />
        <input
            name="<% $helper->fieldname($actor, 'name', 'o2') %>"
            type="text"
            value="<% $actor->name %>"
        />
        <input
            name="<% $helper->fieldname($role, undef, 'o3', {
                film_id  => 'o1',
                actor_id => 'o2'
            }) %>"
            type="hidden"
            value="dummy"
        />
    </form>




In your controller (or cool helper module, used in your controller)

    my @objects = $helper->formdata_to_objects(\%querystring);
    foreach my $object ( @objects ) {
        # Assert and Manupulate $object as you like
        $object->insert_or_update;
    }

DESCRIPTION

Top

Introduction

DBIx::Class::FormTools is a data serializer, that can convert HTML formdata to DBIx::Class objects based on element names created with DBIx::Class::FormTools.

It uses user supplied object ids to connect the objects with each-other. The objects do not need to exist on beforehand.

The module is not ment to be used directly, although it can of-course be done as seen in the above example, but rather used as a utility module in a Catalyst helper module or other equivalent framework.

Connecting the dots - The problem at hand

Creating a form with data from one object and storing it in a database is easy, and several modules that does this quite well already exists on CPAN.

What I am trying to accomplish here, is to allow multiple objects to be created and updated in the same form - This includes the relations between the objects i.e. "connecting the dots".

Non-existent ids - Enter object_id

When converting the formdata to objects, we need "something" to identify the objects by, and sometimes we also need this "something" to point to another object in the formdata to signify a relation. For this purpose we have the object_id which is user definable and can be whatever you like.

METHODS

Top

new

Arguments: { schema => $schema }

Creates new form helper

    my $helper = DBIx::Class::FormTools->new({ schema => $schema });

schema

Arguments: None

Returns the schema

    my $helper = $helper->schema;

fieldname

Arguments: $object, $accessor, $object_id, $foreign_object_ids

    my $name_film  = $helper->fieldname($film, 'title', 'o1');
    my $name_actor = $helper->fieldname($actor, 'name', 'o2');
    my $name_role  = $helper->fieldname($role, undef,'o3',
        { film_id => 'o1', actor_id => 'o2' }
    );
    my $name_role  = $helper->fieldname($role,'charater','o3',
        { film_id => 'o1', actor_id => 'o2' }
    );

Creates a unique form field name for use in an HTML form.

$object

The object you wish to create a key for.

$accessor

The attribute in the object you wish to create a key for.

$object_id

A unique string identifying a specific object in the form.

$foreign_object_ids

A HASHREF containing attribute => object_id pairs, use this to connect objects with each-other as seen in the above example.

formdata_to_objects

Arguments: \%formdata

    my @objects = $helper->formdata_to_objects($formdata);

Turn formdata(a querystring) in the form of a HASHREF into an ARRAY of DBIx::Class objects.

meta

This is a method which provides access to the current class's metaclass.

CAVEATS

Top

Transactions

When using this module it is prudent that you use a database that supports transactions.

The reason why this is important, is that when calling formdata_to_objects, DBIx::Class::Row->create() is called foreach nonexistent object in order to get the primary key filled in. This call to create results in a SQL insert statement, and might leave you with one object successfully put into the database and one that generates an error - Transactions will allow you to examine the ARRAY of objects returned from formdata_to_objects before actually storing them in the database.

Automatic Primary Key generation

You must use DBIx::Class::PK::Auto, otherwise the formdata_to_objects will fail when creating new objects, as it is unable to determine the value for the primary key, and therefore is unable to connect the object to any related objects in the form.

BUGS AND LIMITATIONS

Top

No bugs have been reported.

Please report any bugs or feature requests to bug-dbix-class-formtools@rt.cpan.org, or through the web interface at http://rt.cpan.org.

AUTHOR

Top

David Jack Olrik <djo@cpan.org>

LICENCE AND COPYRIGHT

Top

TODO

Top

* Add form object, that keeps track of object ids automagickly.
* Add field generator, that can generate HTML/XHTML fields based on the objects in the form object.

SEE ALSO

Top

DBIx::Class DBIx::Class::PK::Auto


DBIx-Class-FormTools documentation Contained in the DBIx-Class-FormTools distribution.
package DBIx::Class::FormTools;

our $VERSION = '0.000007';

use strict;
use warnings;

#use DBIx::Class::FormTools::Form;

use Carp;
use Moose;

has 'schema'    => (is => 'rw', isa => 'Ref');
has '_objects'  => (is => 'rw', isa => 'HashRef');
has '_formdata' => (is => 'rw', isa => 'HashRef');


sub fieldname
{
    my ($self,$object,$attribute,$object_id,$foreign_object_ids) = @_;

    # Get class name
    my $class = $object->source_name || ref($object);
    
    my @primary_keys  = $object->primary_columns;
    
    my %relationships
        = ( map { $_,$object->relationship_info($_) } $object->relationships );

    my %id_fields = ();
    foreach my $primary_key ( @primary_keys ) {
        # Field is foreign key
        if ( exists $relationships{$primary_key} ) {
            $id_fields{$primary_key} = $foreign_object_ids->{$primary_key};
        }
        # Field is local
        else {
            $id_fields{$primary_key}
                = ( ref($object) &&  $object->$primary_key )
                ? $object->$primary_key
                : 'new';
        }
    }

    # Build object key
    my $fieldname = join('|',
        'dbic',
        $object_id,
        $class,
        join(q{;}, map { "$_:".$id_fields{$_} } keys %id_fields),
        ($attribute || ''),
    );

    return($fieldname);
}


sub formdata_to_objects
{
    my ($self,$formdata) = @_;

    # Cleanup old objects
    $self->_objects({});
    $self->_formdata({});

    # Extract all dbic fields
    my @dbic_formkeys = grep { /^dbic\|/ } keys %$formdata;

    my $objects = {};

    # Create a todo list with one entry for each unique objects
    # So we can process them in reverse order of dependency
    my %todolist;

    # Sort data into piles for later object creation/updating
    foreach my $formkey ( @dbic_formkeys ) {
        my ($prefix,$object_id,$class,$id,$attribute) = split(/\|/,$formkey);

        # Store form contents
        $self->_formdata->{$object_id}->{'content'}->{$attribute}
            = $formdata->{$formkey} if $attribute;

        # Build id field
        my %id;
        foreach my $field ( split(/;/,$id) ) {
            my ($key,$value) = split(/:/,$field);
            $id{$key} = $value;
        }

        # Store id field
        $self->_formdata->{$object_id}->{'form_id'} = \%id;

        # Save class name and id in the todo list
        # (hash used to avoid dupes)
        $todolist{"$class|$object_id"} = {
            class     => $class,
            object_id => $object_id,
        };
    }

    # Flatten todo hash into a todolist array
    my @todolist = values %todolist;

    # Build objects from form data
    my @objects;
    foreach my $todo ( @todolist ) {
        my $object = $self->_inflate_object(
            $todo->{ 'object_id' },
            $todo->{ 'class'     },
        );        
        push(@objects,$object);
    }

    # Cleanup old objects
    $self->_objects({});
    $self->_formdata({});

    return(@objects);
}


sub _flatten_id
{
    my ($id) = @_;
    
    return join(';', map { $_.':'.$id->{$_} } sort keys %$id);
}


sub _inflate_object
{
    my ($self,$oid,$class) = @_;

    my $attributes;
    my $id;

    # Object exists in form
    if ( exists($self->_formdata->{$oid}) ) {
        $id         = $self->_formdata->{$oid}->{'form_id'};
        $attributes = $self->_formdata->{$oid}->{'content'};
    }
    # Object does not exist in form, use oid as id
    # FIXME -> Should this be removed ?
    else {
        $id = { id => $oid };
    }

    # Return object if is already inflated
    return $self->_objects->{$class}->{$oid}
        if $self->_objects->{$class}
        && $self->_objects->{$class}->{$oid};

    # Inflate foreign fields that map to a *single* column
    my $relationships = {
        map { $_,$self->schema->source($class)->relationship_info($_) } 
        $self->schema->source($class)->relationships
    };
    foreach my $foreign_accessor ( keys %$relationships ) {
        # Resolve foreign class name
        my $foreign_class = $self->schema->source($relationships
                                                  ->{$foreign_accessor}
                                                  ->{'class'}
                                                  )->source_name;

        my $foreign_relation_type = $relationships->{$foreign_accessor}
                                                  ->{'attrs'}
                                                  ->{'accessor'};

        # Do not process multicolumn relationships, they will be processed
        # seperatly when the object to which they relate is inflated
        # I.e. only process "local" attributes
        next if $foreign_relation_type eq 'multi';

        # Lookup foreign object id
        # FIXME: Should we really look in both places?
        my $foreign_oid = ( $self->_formdata
                                 ->{$oid}
                                 ->{'form_id'}
                                 ->{$foreign_accessor} )
                        ?   $self->_formdata
                                 ->{$oid}
                                 ->{'form_id'}
                                 ->{$foreign_accessor}
                        :   $self->_formdata
                                 ->{$oid}
                                 ->{'content'}
                                 ->{$foreign_accessor};
        # No id found, no inflate needed
        next unless $foreign_oid;

        my $foreign_object = $self->_inflate_object(
            $foreign_oid,
            $foreign_class,
        );

        # Store object for later use
        $self->_objects->{$foreign_class}->{$oid}
            = $foreign_object;

        # If the field is part of the id then store it there as well
        $id->{$foreign_accessor} = $foreign_object->id
            if exists $id->{$foreign_accessor};

        # Store the foreign object with all the other object data
        # FIME: Shouldn't I be able to just pass the whole object here ?
        $attributes->{$foreign_accessor} = $foreign_object->id;
    }
    # All foreign objects have been now been inflated

    # Look up object in memory
    my $object = $self->_objects->{$class}->{$oid};

    # Lookup in object in db
    unless ( $object ) {
        my $source = $self->schema->source($class);
        # Don't lookup object if id is 'new'
        $object = $self->schema->resultset($source->source_name)->find($id)
            unless grep { $id->{$_} eq 'new' } $source->primary_columns;
    }

    # Still no object, the create it
    unless ( $object ) {
        $object = $self->schema->resultset($class)->create($attributes);
    }

    # If we have a object update it with form data, if it exists
    $object->set_columns($attributes) if $attributes && $object;

    # Store object for later use
    if ( $id && $object ) {
        $self->_objects->{$class}->{$oid} = $object;
    }

    return($object);
}


# sub form
# {
#     my $self = shift;
#     my $form = DBIx::Class::FormTools::Form->new({});
#     
#     return($form);
# }


1; # Magic true value required at end of module
__END__