I have a content type called "assembly" that contains one or more node references to content types of "component" and/or "assembly". This creates a collection of trees, in which the root of each tree is an assembly and the leaves of each tree are components.

When I'm viewing an assembly, I would like to also output a "BOM" (Bill Of Materials) view that takes the assembly as an argument and presents that assembly's complete list of leaf components.

I guess I would have to search the entire tree for components each time I display the tree. Setting aside for now issues of performance, how could I go about implementing such a view?

Comments

caschbre’s picture

A view can definitely use the node reference as an argument. You can also take a look at the views_attach module. It sort of depends on how you want to display the BOM.

wuertele’s picture

Yes, views attach would allow me to attach a view to a node (so would viewreference). But the problem is not how to make a node also display a view, the problem is how to create a view that can list only those components which are leaves of the tree rooted by a given node.

For example, I might have five nodes:

node/1 (assembly) has a reference to node/2 (assembly) and node/3 (component)
node/2 (assembly) has a reference to node/4 (component) and node/5 (component)

I would want my BOM view to take a node id argument and list all the components of the tree rooted by that node. In this case, if the argument was node/1, the BOM would be

node/3
node/4
node/5

Whereas if the argument was node/2, the BOM would be

node/4
node/5

There could be an arbitrary number of nodereferences linking an assembly to the leaves of its tree. How would I specify the filter for such a view?

caschbre’s picture

Hmm... the direction of your node reference definitely makes a difference in how to use them with views. Let's ignore that an assembly can point to an assembly and focus on how to display components that are referenced directly by an assembly.

Assembly --> Component
The arrow represents that the assembly points to the component. So your assembly node has a list of component node IDs.

Now you'll want to create a view that accepts NIDs as an argument. Click on the argument and then select 'provide default argument' and then 'PHP Code'. You'll need to write a bit of PHP code that looks at the current node being displayed, grabs the list of NIDs from the node reference field, and then format those (not sure what that would look like) as values to be used for the argument.

Assembly --> Assembly --> Component
Since you only want to display components but you may have a node reference to an assembly you'll have to modify your PHP code (from above) a bit. Instead of simply taking your NIDs and using them as arguments you're going to have to look at each NID and check if it is a component or an assembly. If it is a component then you simply add it to your list of arguments. If it is an assembly then you'll have to load the details of that assembly node and get the node reference values.

You're going to have to do this all recursively until you're left with just component NIDs. This could get expensive (performance-wise) and you're keeping PHP code in the database. So you may want to look at writing a little module that can has the code to accept a NID and return all the referenced component NIDs, etc.

wuertele’s picture

Thanks, I think I understand your suggestion. Although I've not written a module before, I'm definitely motivated to learn how for this application. Why is storing PHP code in a module better than storing it in the database?

Also, since there will be many more reads of the nodes than writes, I wonder if I could optimize the recursive activity by only computing a BOM when a change has been made to an assembly? If I have the following trees:

Assembly1 --> Assembly3
Assembly2 --> Assembly3
Assembly3 --> Component1
Assembly4 --> Component2

Let's say that I edit Assembly3 to add a reference to Component2. Ideally that would trigger a recomputation of the BOMs for Assembly1 and Assembly2 (not Assembly4), and store/cache the new component lists in the DB for Assembly1 and Assembly2.

Can I trigger such a recomputation with an edit/save? Can I store computed data for retrieval later by the node view? Would a module be the best way to acheive this functionality?

caschbre’s picture

Ignore the module initially and just put the PHP code in the view. Make sure it works first. When it does then you can decide if a module is worth it. I don't remember the specifics about why to put code in modules vs the database other than that the system can process it faster that way.

The system will also compute on the fly. I wouldn't worry about trying to store it / recompute it. You can use views caching to cache it.

wuertele’s picture

Now I just need to figure out how to trigger updates of other nodes.

Here's how I got most of what I need:

I installed the "Computed Field" module, and in my Assembly type I added a field named "BOM" with type "computed". The "computed" type allows you to insert two chunks of arbitrary PHP code: one chunk that gets run at node update time, and another chunk that gets run at display time.

In the update-time chunk, I had a for loop iterate over the node's noderefs. For each child component, it added that component's nid to a list named "components[]". For each child assembly, it looked into the child assembly's BOM field, exploded its list of components, and added them all to "components[]". After the loop was done, it imploded components[] into the computed field, and that is what gets stored. Any time an assembly or component node gets updated, I have to manually update all the parent nodes to aggregate the BOM contents.

In the display PHP chunk, I explode the BOM field into components and format them in a table. It looks great!

I would like to not have to manually update all the parent nodes (and their parents, and so on...) every time I change a child assembly or component. I can have the update-time PHP enumerate all the parents, that works fine. And I thought that I could just have the update-time PHP call content_update($parent_nid) to get the parent to update. And in fact, the computed field code in the parent does run and comuptes the correct BOM for the parent. But it does not actually save the computed value in the database the way a computed field does when I save a node manually from its node//edit page.

Is there a higher-level drupal API that I should call which is equivalent to clicking on the "Save" button on nod//edit?

caschbre’s picture

You shouldn't have to use a computed field. If you're creating a view then that will calculate everything for you on the fly. The PHP code in the view will start at the assembly being displayed and recursively work it's way down the assembly/component references to generate the BOM list.

wuertele’s picture

I think I see what you're saying about using views, but I'm worried about performance. If I use Computed Field, the computation is only done at the time of updating an assembly node. If I use a view, the computation will be done at the time of displaying the assembly node.

I suppose the view might be cachable? How would the caching be aware that the change to a child of a child of an assembly requires invalidation of the cache?

Even if the caching could be made to be properly sensitive to invalidations, it would still be recomputed at display time instead of at update time.

caschbre’s picture

It's hard to say what the performance will be like. How many assembly and component nodes do you expect? And how many levels deep will the PHP code have to recurse through?

I'd first start with using views, if for nothing else, to at least test the logic and get things presentable.

If you do find performance problems then start looking at alternatives.

wuertele’s picture

It's just that I already implemented everything with Computed Field, and it is working and looking great. The only feature it does not have is the ability to get node_save($parent_node) to actually store the parent's recomputed BOM in the database.