Jump to content

How to call only one instance of each entry a relational data field


kathep
 Share

Recommended Posts

Hey all

I recently resolved a problem displaying relational category information (see here). I now have another challenge using similar code.

I have set up a sort of 'breadcrumb of categories' for certain items.

It refers to these templates and fields:

  • design_skill_categories.php
    • title
  • design_technique.php
    • design_skill_categories (a Multiple Page field referencing design_skill_categories.php)
    • title
  • readings.php
    • design_principle
    • design_technique (a Multiple Page field referencing design_technique.php)
    • psychology_topics
The following code works great when there are only a few items selected in the design_technique field: 
$subject = '';

if($page->psychology_topics instanceof PageArray) {
foreach($page->psychology_topics as $test) {
$subject = " > " . $page->psychology_topics->implode(", ", "title");
}
}

if($page->design_principle instanceof PageArray) {
foreach($page->design_principle as $test) {
$subject1 = " > " . $page->design_principle->implode(", ", "title");
}
}

//design_skill_category to show for each design_technique
$design_technique = $page->design_technique;

if($design_technique instanceof PageArray) {
foreach($design_technique as $test_skill) {
$import_skill .= " > " . $test_skill->design_skill_category->implode(" / ", "title") ;}
}

if($design_technique instanceof PageArray) {
foreach($page->design_technique as $test) {
$subject2 =  " > " . $page->design_technique->implode(", ", "title");
}
}
 
$reading_category = "<p><strong>Subject:</strong> " . $page->reading_categories->title . $subject . $subject1 . $import_skill . $subject2 . "</p>";
 
if($reading_books instanceof Page) { $content = $reading_category; }
 

When there are only a few techniques, this is the output I get (which is just what I want):

post-2947-0-91897500-1419966738_thumb.pn

The basic format is 'level one sorting > level two sorting / separated by backslashes > level three sorting, separated by commas'.

However, when a page using the template readings.php has several techniques associated (field: design_technique) that in turn have several categories (field: design_skill_categories), it doesn't work. See below:

post-2947-0-41513200-1419966610_thumb.pn

In the second example, my system is broken and changes to: 'level one sorting > level two sorting / related to design_technique 1 > level two sorting / related to design_technique 2 > level two sorting / related to design technique 3 > level three sorting, design_technique 2, etc'.

I would like to alter my code so that all the categories (field: design_skill_categories) related to all of the techniques (field:design_technique) display in between a beginning '>' and an end '>', and each category only displays once. 

Do any of you know a way to write the code to achieve this?

I hope I have been doing things the long (and probably hard) way, and that someone has a relatively easy solution. *crosses fingers*

  • Like 1
Link to comment
Share on other sites

Narrowing down the problem

Whoa, looking back on this code, it is confusing! And I'm the one who wrote it!

I will try to write the problem I have in a clearer way.

Using php.net and googling the forums a lot, I have identified a possible solution. I don't know how to implement it myself, but hopefully someone will have an answer...

1. The problem

Narrowing down (and clarifying) the code involved in the problem:

// calls the MultiPage field book_title_from_list (which references reading_books.php)
$reading_books = $page->book_title_from_list;

// calls the MultiPage field design_technique (which references design_technique.php) 
$design_technique = $page->design_technique;   

// for every instance of a design_technique, show a list of every category associated with that technique
// shows each list one after the other
if($design_technique instanceof PageArray) {
    foreach($design_technique as $test_skill) {
        $import_skill .= " > " . $test_skill->design_skill_category->implode(" / ", "title") ;
    }
}

// this is its own call because I add some other arrays (that are not relevant to this example, so not shown here)
$reading_category = $import_skill ;

// if a book title has been selected, show the array $reading_category in main content area
if($reading_books instanceof Page) { $content = $reading_category; }
I have commented it as best I know how. Please improve it if you notice any silly errors!

The incorrect code is this:

if($design_technique instanceof PageArray) {
    foreach($design_technique as $test_skill) {
        $import_skill .= " > " . $test_skill->design_skill_category->implode(" / ", "title") ;
    }
}
I see now that this code is kind of dumb. The code itself tells me the output will not be what I want.

I would like to alter my code so that all the categories (field: design_skill_categories) related to all of the techniques (field:design_technique) display in between a beginning '>' and an end '>', and each category only displays once. 

 

2. The potential solution

I think I need to break it down into two chunks, something like this:

// make a list every design_technique selected (only for calculation, not for output)
$list = *some code*

// identify every design_skill_category associated with $list
if($list *some code*) {
    *some code* {
        $import_skill .= " > " . *some code*->design_skill_category->implode(" / ", "title") ;
    }
}
Hopefully such code (when gaps are filled in) would avoid repeated output of design_skill_category items when they are referred to by multiple design_technique items.

Can you offer a link to a php reference or tutorial that could get me another step closer to filling in the blanks? Or even a solution?

Link to comment
Share on other sites

It could be something like that:

if($design_technique instanceof PageArray) {

    // create array for collecting output
    $outItems = array();

    foreach($design_technique as $test_skill) {
        
        // array collecting categories
        $alreadyFoundCategories = array();

        foreach($test_skill->design_skill_category as $example) {
            // is already processed, so skip this one
            if(isset($alreadyFoundCategories["{$example->title}"])) continue;
            
            // add it to the list
            $alreadyFoundCategories["{$example->title}"] = $example->title;
            
            // collect for output
            $outItems[] = $example->title;
        }
    }

    // create the output
    $import_skill .= " > " . implode(" / ", $outItems);

}

 
 
You can shorten this by using an associative array for $outItems and drop collecting items in $alreadyFoundCategories. Also I'm not really sure on wich level the items reside you want avoid to be collected twice. So you have to experiment with this snippet.

if($design_technique instanceof PageArray) {

    // create array for collecting output
    $outItems = array();

    foreach($design_technique as $test_skill) {
        
        foreach($test_skill->design_skill_category as $example) {
            // collect for output
            $outItems["{$example->title}"] = $example->title;
        }

    }

    // create the output
    $import_skill .= " > " . implode(" / ", $outItems);
}
  • Like 3
Link to comment
Share on other sites

  • 2 weeks later...

It could be something like that:

if($design_technique instanceof PageArray) {

    // create array for collecting output
    $outItems = array();

    foreach($design_technique as $test_skill) {
        
        // array collecting categories
        $alreadyFoundCategories = array();

        foreach($test_skill->design_skill_category as $example) {
            // is already processed, so skip this one
            if(isset($alreadyFoundCategories["{$example->title}"])) continue;
            
            // add it to the list
            $alreadyFoundCategories["{$example->title}"] = $example->title;
            
            // collect for output
            $outItems[] = $example->title;
        }
    }

    // create the output
    $import_skill .= " > " . implode(" / ", $outItems);

}

Hey @horst,

So I finally tested this out. It works, sort of.   :undecided:

With the suggested code, I get exactly the same output as with my original code... duplicates from the 'design_skill_category' field that I am trying to avoid. I guess that this snippet is not doing what it is commented to do:

// is already processed, so skip this one
if(isset($alreadyFoundCategories["{$example->title}"])) continue;

  

But this php is far more advanced than my ability, so it is just a guess!

If anyone can notice a change to @horst's code above, or suggest another direction for me to research, please let me know!

Link to comment
Share on other sites

RESOLVED

Here is the code I used to fix the duplicate problem. It is mostly @horst's code suggestion from above, with something added (from this stackoverflow post) and something taken away.

// beginning of new design category code
if($design_technique instanceof PageArray) {

		// create array for collecting output
		$outItems = array();

		foreach($design_technique as $test_skill) {

				// array collecting categories
				$alreadyFoundCategories = array();

				foreach($test_skill->design_skill_category as $example) {
						// add it to the list
						$alreadyFoundCategories["{$example->title}"] = $example->title;

						// collect for output
						$outItems[] = $example->title;
				}
		}

		// create the output
		$import_skill .= implode(" / ", $outItems);
		
		$import_skill_clean .= implode(' / ',array_unique(explode(' / ', $import_skill)));
}
//end of new design category code

I didn't know about array_unique or implode then explode tricks until this afternoon. Another lesson in php from processwire implementation.  :)

I am cobbling together code from various sources until it works, without really understanding _how_ it works yet. Please forgive ugly syntax and nonsense in my code, and feel free to offer improvements.

  • Like 4
Link to comment
Share on other sites

Hey @kathep,

glad you have solved it. But I wonder if it couldn't be done shorter, and also if not one of my examples also should work.

Do you also have tried my second example?

I think this one should work, regardless if I have understand the hirarchycal order of your data or not. If you like, please try out and compare the following examples:

if($design_technique instanceof PageArray) {

  # first run my example 1) but put the $alreadyFoundCategories = array one level up !!!

    // create array for collecting output
    $outItems = array();
    // array collecting categories
    $alreadyFoundCategories = array();   // this is moved from **) to this line

    foreach($design_technique as $test_skill) {
        
        // **) here was the "$alreadyFoundCategories = array();" before

        foreach($test_skill->design_skill_category as $example) {
            // is already processed, so skip this one
            if(isset($alreadyFoundCategories["{$example->title}"])) continue;
            // add it to the list
            $alreadyFoundCategories[$example->title] = $example->title;
            // collect for output
            $outItems[] = $example->title;
        }
    }

    // collect for comparision
    $outItems1 = $outItems;
 

  # second run is my example 2)

    // create array for collecting output
    $outItems = array();
    foreach($design_technique as $test_skill) {
        foreach($test_skill->design_skill_category as $example) {
            $outItems[$example->title] = $example->title;  
            // $outItems will be created as associative array, $key => value
            // we use the value (your title) also for the key 
            // therefore each following item with a identical value will overwrite the previous one 
            // and each items stays unique, as this is the nature of arrays: they cannot have keys with the same name or index !!!
            // therefore there is no need for array_unique ! But let us see 
        }
    }

    // save it and create unique arrays for comparision:
    $outItems2 = $outItems;
    // we pass $outItems1 and $outItems2 through array_unique(), what will remove any duplicate value if there is any !
    $outItems1u = array_unique($outItems1);
    $outItems2u = array_unique($outItems2);

    // now we put some debug message out
    $outMsg = "<pre>\n";
    $outMsg .= "Method 1 collected " . count($outItems1) . " items\n";
    $outMsg .= "Method 2 collected " . count($outItems2) . " items\n\n";
    $outMsg .= "UniqueArray1 has " . count($outItems1u) . " items\n";
    $outMsg .= "UniqueArray2 has " . count($outItems2u) . " items\n\n";

    $outMsg .= "Method 1 seems to work " . (count($outItems1) == count($outItems1u) ? 'correct!' : 'wrong! Sorry.') . "\n";
    $outMsg .= "Method 2 seems to work " . (count($outItems2) == count($outItems2u) ? 'correct!' : 'wrong! Sorry.') . "\n";

    echo $outMsg . "\n\n</pre>";
}

Edited by horst
  • Like 1
Link to comment
Share on other sites

Hey @horst

Oh wow, you are thorough!

I only tried your first example, the long one. Honestly, I did not feel confident to try the second one, as I find the long version hard to understand, let alone the second one with the shorthand! 

Your latest example with even more markup helps me understand the purpose of each line. When I have some experimenting time, I will play around more with this code. One day I hope to understand all of it.  :)

Link to comment
Share on other sites

Oh wow, you are thorough!

Really? :)

---

If you can find the time, you should drop in the code from my previous post and try it. - But also there is nothing to say against using array_unique. :)

The only thing with your code is that you don't need to use this cool implode / explode trick because you initially have the array. So you first can run $outItems through array_unique() and then implode() it to get your outputstring. And when using array_unique() you don't need even to try to collect and compare category titles:

// beginning of new design category code
if($design_technique instanceof PageArray) {

        // create array for collecting output
        $outItems = array();

        foreach($design_technique as $test_skill) {
                foreach($test_skill->design_skill_category as $example) {
                        // collect for output
                        $outItems[] = $example->title;
                }
        }

        // create the output
        $import_skill .= implode(" / ", array_unique($outItems));  // implode gets the result from array_unique here

}
//end of new design category code

PS: But you should bookmark or somehow store this trick (explode / array_unique / implode) for later use if you once only have a string list with double items. I have bookmarked it (the link in the browser favourites and the code in my brain) :)

  • Like 4
Link to comment
Share on other sites

So I thought that string trick was neat and googled it. Since this topic is solved, it might be of interest that this SO thread suggests using array_flip() and imploding the keys instead, because array_unique() sorts the array first, and thus runs a lot slower. http://stackoverflow.com/a/5134212

Array_flip() swaps the keys and values of the array, thereby eliminating duplicates. Of course this comes at the price of some legibility.

Just a tidbit, but seems useful if you do this kind of stuff, especially with large n.

  • Like 1
Link to comment
Share on other sites

If you can find the time, you should drop in the code from my previous post and try it. - But also there is nothing to say against using array_unique. :)

Ok, good to know.

And thank you for explaining what is wrong with my code, and how to shorten it. It seems every time I have a problem in processwire I discover many more cool things than I originally hoped for.  :grin:

@Jan Romero ah, ok. Good to know. I guess me and @horst have another Stack Overflow link to bookmark.

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...