Hello everybody.

I am stuck on this one for days now and I am starting to think it is not doable (would be amazing and really bad) or I miss something obvious.

The problem :

I need to create a "node" of some type WHEN a node of another type is created and bind them together.
One is a profile based on content profile and the other one is a "scorecard" of sort that keep record of various consumption of services.
Every profile as a scorecard attached, no scorecard exists without relation to a profile (but scorecards can be detached for archiving and not be anymore the one the profile point to)
It is typical 1-1 relationship, or aggregation in OO parlance.
I tried to do it with rules but failed and since I got tired of having no feedback about what happened, I decided to do it by code.
Sounds obvious and sounds easy.

When node of type MasterProfile is about to be saved to database => create a scorecard node => and put the MasterProfile fieldref on it.

Of course it don't work like that because at creation time, nid are not given to nodes and node id's are needed to build the reference.
I won't delve into that aspect of drupal design, not assigning an identity to entities/object as soon as created seems to be the root of many evils, a least in my book, but that is the way it is.

In the end the process is more like this

wait for not the masterProfile node to be inserted
=> build and save a ScoreCard node
=> assign its id to the node ref on the masterProfile
=> re-save the MasterProfile node

I thought that the place to do such things would be in the hook_nodeapi, in the insert op for instance....

But I can't understand the node creation cycle and make sence of what I see in my traces :

When I create the ScoreCard node and try to save it the hook_nodeapi event if fired again BUT NOT WITH THE NEW NODE, BUT THE MasterProfile node AGAIN !!!!.

Basically save_node (or execute node) seems to be a joke of a function and in fact rely on many statics or "global stuff" that, in the end, throws it away.
(Joke of a function because parameters don't seems to be the only inputs so it is not a function, and side-effects are crazy to boot).

What I would have expected is hook_nodeapi to be called with the newly user created MasterProfile node then at first save_node on the ScoreCardNode, fired with its node parameter set on this new one, then back inside the hook_nodeapi call (savenode function return ) and probably fired again when I save the MasterProfile node updated with the ref to the scoreCardNode.

Instead I have infinite loops where the hook_nodeapi is called again and again with COPY (deep copy) of the MasterProfile node.
Each time the hook_nodeapi is fired by the save_node or node_execute call it has a new copy (probably from cache) of the MasterProfile node with NONE of the changes I made beforehand in the hook_nodepai call.

If in hook_nodeapi 'insert' op, I do something like that :

 $node->onefield="xxxx"
 $scoreCardNode=createScoreCaredNode();
 save_node($scoreCardNode);

I end up back inside the hook_nodeapi with a new copy of the $node without the onefield set to "xxx".

What am I doing wrong ?

Is there a good way to do such thing, because that sounds like a pretty basic need ?

I looked all around google but there is very little documentation on the real life cycle and dependency of the multistage node save and the order and construction of hook calls.
Plus people contradict themselves and most knowledge seems quite empirical.

What does content_insert ?
How is it related to node_save ?
Why use the drupal_execute call when you can have $save_node if your data are coherent ?
Do we need to populate all fields on a node regarding its schema, especially CCK_fields?
Is "$newnode->is_new=1;" a vital part of the process ?
Until what time god's bar is open ?

I am open to every ideas, questions, suggestions, and I can produce code and such but I wanted to explain the problem to know if I am missing something obvious or if people have already encoutered the problem.
PS : Any google search with the words cck fields, node create, node cache, hook_api, drupal_execute, node_save and combinations of them has probably been done already ;)

The last thing that make me suspicious about doing that is that every module I have seen that could do that, don't (
Referential Integrity for CCK http://drupal.org/project/cck_referential_integrity , Autocreate Node Reference http://drupal.org/project/autocreate ) or use tricks to do that at display time though rendering tricks. It is not acceptable in my case because I need data coherence, and one can look for all scorecards without going through masterProfile nodes.
but if you know of a module that does it or something like it I'll gladly try to understand the code.

Thanks a lot for your time.

Comments

cog.rusty’s picture

I am no developer, but are you sure that using hook_nodeapi() is a good idea? You want to act on you own nodes and not on any node on the system which performs the op, isn't that right?

At least this is what the documentation seems to say.

------ Edited to add:
You may want to check how these modules do similar things:
http://drupal.org/project/popups_reference
http://drupal.org/project/noderelationships - the "Create and reference" feature.

Jaypan’s picture

I'll give you a basic rundown of how to do this, though I'm not going to write the code for you, you will have to do a bit of searching to find the specifics. But at least I can point you in the right direction.

First, you need to make sure both content types exist in your Drupal installation. You will also need to make sure you have created a table (using hook_install(), hook_uninstall() and hook_schema()) that has two columns, one for your masterprofile NID and one for your scorecard NID. I'll call this table 'masterprofile_scorecards' for this explanation.

Next, you use hook_nodeapi() and when $op = 'insert', you will create your new node:

function my_module_nodeapi() // leaving out the arguments - you will have to put them in
{
  if($op == 'insert' && $node->type == 'masterprofile')
  {
     // You will create the new node here
  }
}

When creating a new node, you first need to create an object, and populate it with the relevant node data. Search for 'creating nodes programmatically' on this site, and you will find some references. I personally prefer the method that uses the function node_save().

This next section will go inside the if() statement in the above hook_nodeapi() function:


$scorecard = new StdClass;
$scorecard->type = 'scorecard';
// populate the rest of your scorecard node object here
// next we save the new node to the database
node_save($scorecard);
// now your $node object holds the NID of the masterprofile node, and your $scorecard object holds the NID of the scorecard node. So you save them do the database
db_query('INSERT INTO {masterprofile_scorecards} (masterprofile_nid, scorecard_nid) VALUES (%d, %d)', $node->nid, $scorecard->nid);

Now you have two newly created nodes, and a reference to each other in the masterprofile_scorecards table.

Annakan’s picture

Thanks for the reply.

I mostly did that but has stated my problem is that when I do the node_save($scorecard) the hook_nodeapi is called AGAIN but not with $&node set to the newly created $scorecard node, something that could seem logical, but with a DEEP COPY of my MasterProfile node, the one that triggered the need to create a $scorecard to attach to.

that's my point node_save($scorecard) don't seem to behave like it save a node "in isolation", somehow some contextual information seems to trigger hook_nodeapi with the wrong object as &node parameter.

if you think it could help I could clean up and post my code but it is mostly what you suggest (mostly because I don't do the direct insert in a table since it is a reference field who's DB layout is not obvious, and besides doing that means no event pertaining to other modules on insertion would be fired right ?)

Thanks a lot for your time.

Jaypan’s picture

Post your code, and explain how you are saving the reference of the two nodes to each other.

Annakan’s picture

thanks for your help, and sorry for the delay in answering, a close friend passed away last week and it was a bit hectic.
I hacked a solution with rules and the node backreference module (described here http://groups.drupal.org/node/71243#comment-226988) but it IS a hack and I would love to understand and make work the "module" solution I describe here.
I will clean up and post my code here ASAP.

Thanks again for the help

Annakan’s picture

Ok here is the cleaned up a bit code to express my first attempt at this.

I later tried lots of things to prevent the "loop" call with the original node copy, using static variable and such but I cleaned all that up.

I left two way to try to correctly instantiate the "scorecard kind" node I want to associate to the "profile node" to show the various trial and methods I played with, one in comments.

Tell me if it helps or if I can provide more information.

function mymod_dataintegrity_nodeapi(&$node, $op, $a3=NULL, $a4=NULL) {
	switch ($op) {
	case 'prepare' :	
	break;
	case 'validate':
		break;
	case 'presave' :
  break;		
	case 'insert':
		if (($node->type='mymod_profile') && empty($node->field_myprofile_ref_scorecard[0]['nid']) {
			new_scorecard=_build_new_scorecard($node, $node->title . '_fiche_conso');
			$node->field_myprofile_ref_scorecard[0]['nid']=new_scorecard->nid;
			node_save($node); // to save again the newly changed node with the field filled (so no loop)
					}
		break;
	}		
}


function _build_new_scorecard(&$nodetoref, $title_hint=NULL){
	$newnode = new stdClass();
	$newnode->is_new=1;
  $newnode->title = empty($title_hint)?$title_hint:"titre non renseigné";
	$newnode->type = 'scorecard_type';
  $newnode->uid =$nodetoref->uid;
  $newnode->teaser = "";
  $newnode->status = 0; //unpublished
  $newnode->created = time();
  $newnode->changed = time();
	$contenttype = content_types($newnode->type);
  foreach ($contenttype['fields'] as $fieldname => $field) {
      if(isset($field['widget']['default_value'])) {
         $newnode->$fieldname = $field['widget']['default_value'];
      }
   }
	// I don't undestand the exact purpose of this two calls, sometime paired sometime not, nobody seems to know their precise purpose, I was not able to dig into the source to fully understand their specifics
	$newnode=node_submit($newnode);
	content_insert($newnode);
	//node_save($node); was also tried at first
//	cache_clear_all(); // does not change anything
return $newnode;

 /* I also tried another way with drupal_execute and read the suggestions here http://drupal.org/node/260934 so I tried the alternate drupal execute without static datas, to no avail, same behavior, :
 	mymod_dataintegrity_nodeapi is called again from the "save/drupal execute" call with the WRONG &node still set to a COPY of the "mymod_profile" node that triggered the "case 'insert' " treatement in the first place*/
 /*
 $form_state = array();
 module_load_include('inc', 'node', 'node.pages');  // new for Drupal 6
 $nodeType = array('type' => 'scorecard_type'); // a variable holding the content type
 $form_state['values']['type'] = 'fiche_consommation_mymod'; // the type of the node to be created
 $form_state['values']['status'] = 1; // set the node's status to Published, or set to 0 for unpublished
 $form_state['values']['title'] = 'Test fiche_consommation_mymod'.rand();   // the node's title
 $form_state['values']['op'] = t('Save');  // this seems to be a required value
 $form_state['values']['name'] = 'admin';
 //include_once './alternate_execute.inc';
 module_load_include('inc','mymod_dataintegrity','alternate_execute'); // from http://drupal.org/node/260934, you can remove this line and call simply drupal_execute instead of _drupal_execute next line, for the same result
 $err = _drupal_execute('scorecard_mymod_node_form', $form_state, (object) $nodeType);*/
}

Thanks a lot for your help.

da_solver’s picture

Hi,
Haven't looked through all the code, but noticed a typical problem:

if (($node->type='mymod_profile')

Should be

if (($node->type=='mymod_profile')

Right? Your code assigns a value to the "type" attribute. Don't you want to test that the node->type attribute is equal to something ==?

Jaypan’s picture

That's the problem alright. That statement as it is now will always return true, which means it will cause a loop.

Itangalo’s picture

Inspired by a discussion at http://groups.drupal.org/node/71243 I made a small solution to this problem using Rules (http://drupal.org/project/rules).

It can be viewed, downloaded and tried at the Rules documentation: http://drupal.org/node/820938