Re: [Fwd: RE: Multiple Tables]

0 views
Skip to first unread message

Alan Pinstein

unread,
Nov 18, 2007, 2:44:36 PM11/18/07
to phocoa...@googlegroups.com, Rich Fallis, Chris Miller
> We're trying to build a page that can display data from multiple
> tables. Rich sent me the following :
>
> <rich>
>
> Running :
>
> phocoa createSkeletonFromPropel
>
> allows for the binding of one table to one module. Alan cautions
> against
> running it more than once on the same module in the blog guide. It
> didn't
> work when I tried repeating it, though it might be possible to feed
> it other
> params etc. and have it work.

Yes, the builder is really only for single-table crud. However, it is
possible (and pretty easy) to add data from joins.

> The blog is the only database example and it only uses a single
> table. It seems that manipulating data in multiple tables would be
> appropriate in a single module, correct? Propel allows us to do
> joins, were just confused on how to do this in the Phocoa way.
> Follow me?

The answer depends really on the type of join you're doing...

If you're doing a one-to-one join, like user to address where a user
has exactly one address, you can just add the fields inline to the
form and set up bindings through the join relationship.

For instance, let's say you want to show the address fields inline
with the user data:

Name: XXX
Street: XXX

The bindings for "name" would look like:

name :
class : WFTextField
bindings:
value:
controllerKey: selection
instanceID : Customer
> modelKeyPath : name


and the bindings for "Street" like:

street :
class : WFTextField
bindings:
value:
controllerKey: selection
instanceID : Customer
> modelKeyPath : address.street


The key thing to notice is the quoted modelKeyPath...
"address.street" expands to:

$customer->getAddress()->getStreet()

So that's how you'd do a one-to-one join. I will also point out that
as of propel 1.3, I don't think that one-to-one joins are implicitly
created if there is no related object. That is, if you have a user 1
but no corresponsing address row yet, getAddress() will return NULL.
I "fix" this in my model with:

Customer.php:
public function getAddress($con = null)
{
$addr = parent::getAddress($con);
if ($addr === NULL)
{
$this->aAddress = new Address();
}
return $this->aAddress;
}

This way you are guaranteed the 1-to-1 relationship is intact at all
times and don't have to do any error handling in your controllers.

Now, if you're talking about to-many relationships, you can also do
this in PHOCOA pretty easily.

The keyPaths work the same. Let's say a user can have many blogs, so:

$user->getBlogs() would return an array of all blog posts.

If you wanted to display all of these posts on the "user" page, there
are a couple of ways to do this:

1) Create a separate module for the "list" view of the related
records, and just use WFModuleView to include this data on whichever
views you want to. This is the most modular approach. The only caveat
here is that you need to carefully name your widgets so that you
don't have ID conflicts across the pages. I hope to build a
namespacing solution in the future, but for now it's do-it-yourself
collision-avoidance. Very easy to do as long as you do it from the
beginning. We're PHP programmers, so we're used to this issue :)

2) Build the related items inline on the same module.

In this approach, you can simply create WFDynamic's for the related
rows, and bind them to the related objects through the relationship.
Or you create a separate array controller for the related objects.

A good example is on my virtual tour editing page. A "tour" has
"virtual_tour" objects, the latter being placeholders for the 360-
degree photographs. Thus you want to see all of them on the "edit
tour" page.

In this example, I set up a separate array controller for the related
objects:

Here's how it's set up for the "name" of the virtual_tour object
(which room the photo is of):

in shared.yaml:
VirtualTour :
class : WFArrayController
properties:
automaticallyPreparesContent: false
class : VirtualTour
classIdentifiers : '#arrayIndexes#'

in tour.php:
// set up the arraycontroller's objects
$this->VirtualTour->setContent
(VirtualTourPeer::getImagesForTourOrderedAsc($currentTourId));

in edit.yaml:
virtualTourName :
class : WFDynamic
properties:
arrayController: '#module#VirtualTour'
children :
virtualTourNamePrototype:
class : WFLink
bindings:
label:
controllerKey: '#current#'
instanceID : VirtualTour
modelKeyPath : name
value:
controllerKey: '#current#'
instanceID : VirtualTour
modelKeyPath : virtualTourId
options :
ValuePattern: /panel/virtual_tour/edit/%1%

And finally, the HTML:

<table border="1" cellpadding="3" cellspacing="0"
style="margin: 0 20px 0 10px;">
<tr>
<th colspan="3" align="center">Virtual Tour Images</td>
</tr>
{section name=virtualTours loop=$__module->VirtualTour-
>arrangedObjectCount()}
{if $smarty.section.virtualTours.first}
<tr>
<td align="center">Name</td>
<td align="center">Image</td>
<td align="center">Sequence</td>
</tr>
{/if}
<tr>
<td align="right">{WFDynamic id=virtualTourName}</td>
<td align="left">{WFDynamic id=virtualTourImage}</td>
<td align="left">{WFDynamic id=virtualTourSequence}</
td>
</tr>
{sectionelse}
<tr><td>No Virtual Tours.</td></tr>
{/section}
</table>

Does that make sense?

Alan

Alan Pinstein

unread,
Nov 20, 2007, 10:00:12 AM11/20/07
to phocoa...@googlegroups.com, Chris Miller, Rich Fallis
Hey Rich-

> Let's say we have two tables, 'names' and 'addresses' and, for now
> anyway,
> there joined 1:1. I'd like to display the equivalent of
>
> SELECT * FROM name, address WHERE names.name_id = address.address_id

So this looks like a "list" view, right?

What I always do when I build PHOCOA stuff is determine:

1) What data do I need?
--> Define the object controllers (WFObjectController and/or
WFArrayController)
--> Query the data and load the object controllers
2) What do I want to view?
--> Set up the UI widgets & bindings
3) What actions do I want to take?
--> Set up buttons, forms, etc.
--> Define my action methods

That's the general approach I take.

For this example, I'd do, in YAML:

(NOTE: I am assuming your tables are "Names" and "Addresses" --
although, I'd recommend that you name your tables in the singular
since Propel adds 'es' to various method names for fetching related
objects and your methods will get ugly, like getAddresseses()).

> ---
> Name:
> class: 'WFArrayController'
> properties:
> class: 'Names'
> classIdentifiers: 'nameId'
> selectOnInsert: true
> automaticallyPreparesContent: false

You need only one for the "main" object here... we will access the
joined data through the relationship.

The only time I really ever set up MULTIPLE array controllers is when
I am working with sets of data that aren't 1-to-1. Then you'll need a
way to directly access each "set" via an array controller.

In PHP (in your page delegate's parametersDidLoad()):

$rows = NamesPeer::doSelectJoinAddress(new Criteria);
$page->sharedOutlet('Name')->setContent($rows);

That is all the code you need for this exercise.

For the view, the detail.yaml would be (I'll just do 2 fields)...

Now, since this is a "list-y" view, you need to dynamically build N
widgets, one for each row.

Anytime you have a situation like this, you need to use WFDynamic,
which is a "magic" widget that creates other widgets on-the-fly.

The way WFDynamic works, it is uses a "prototype". A WFDynamic should
contain a single child element of the same ID but followed by
"Prototype".

You set up the WFDynamic to point to your ArrayController, and you
set up the rest of the bindings and widget properties on the prototype.

> firstName:
> class: 'WFDynamic'
> properties:
> arrayController: #module#Names
> children:
> firstNamePrototype:
> class: WFTextField
> bindings:
> value:
> instanceID: 'Names'
> controllerKey: '#current#'
> modelKeyPath: 'firstName'
> address:
> class: 'WFDynamic'
> properties:
> arrayController: #module#Names
> children:
> addressPrototype:
> class: WFTextField
> bindings:
> value:
> instanceID: 'Names'
> controllerKey: '#current#'
> modelKeyPath: 'addresses.firstName'

So, what the above basically does is tell WFDynamic to create a
WFTextField for each "Name" object in the array controller. The
special "#current#" controllerKey tells the WFDynamic that the
"value" of each WFTextField created should bind to the "current" item
in the array controller (current as in the current() function for PHP
arrays, which is where the cursor points). The name of the
ArrayController and the "instanceID" of the prototype should match;
this is how WFDynamic knows to set up the above explained settings.

Then, your view would look like:

> ...
> {section name=items loop=$__module->valueForKeyPath
> ('Names.arrangedObjectCount')}
> <tr>
> <td valign="top">First Name</td>
> <td valign="top">{WFView id="firstName"}</td>
> </tr>
> <tr>
> <td valign="top">Address Id</td>
> <td valign="top">{WFView id="City"}</td>
> </tr>
> {/section}

That's the simplest way to do what you're talking about.

You could then wrap up all of that in a form and have a way to edit
multiple items at once.

Alan

Reply all
Reply to author
Forward
0 new messages