Jump to content

Method(s) to cycle through foreach/compare values of Children


creativejay
 Share

Recommended Posts

I am currently working on a site that lists about two hundred product series, which are displayed to the visitor as a web page.

Beneath each of these Series pages are one or more children, and they may be nearly identical, with a few key differences.

Does anyone have any advice on how I might run through the 'foreach' of the children on the Series' output page, and determine if the field values are the same or unique, and output the unique values while "merging" the common values?

For example:

Product Series A contains:

  • Model A - which has a "color" field value of "blue"
  • Model B - which has a "color" field value of "blue"
  • Model C - which has a "color" field value of "green"
  • Model D - which has a "color" field value of "red"

So in this example, I'd want to list this information as:

Product Series A

[[bunches of info from the Series parent page's fields]]

Specifications

      Color:      Blue (Models A and B), Green (Model C), or red (Model D)

Or, if the values were all the same, it would list just the value without the model mentions. 

Thanks in advance, even if you just have a direction to point me in, I'd appreciate any input. My brain is fried from all the crazy logic I've had to implement for this site already.  :frantics:

Link to comment
Share on other sites


$byColor = array_reduce($children->getArray(), function($carry, $child){

$carry[$child->color][] = $child;

return $carry;

}, array());

$toTextBlocks = array_map(function($color, $children){

$childrenPA = (new PageArray)->import($children);

return "$color (" . $childrenPA->implode(", ", "title") . ")";

}, array_keys($byColor), $byColor);

$line = implode(", ", $toTextBlocks);

  • Like 6
Link to comment
Share on other sites

$byColor = array_reduce($children->getArray(), function($carry, $child){
  $carry[$child->color][] = $child;
  return $carry;
}, array());

$toTextBlocks = array_map(function($color, $children){
  $childrenPA = (new PageArray)->import($children);
  return "$color (" . $children->implode(", ", "title") . ")";
}, array_keys($byColor), $byColor);

$line = implode(", ", $toTextBlocks);

Thank you! It'll take me a bit to wrap my head around this (I have never been strong in array logic), but I wanted to let you know I really appreciate the immediate reply and am trying to implement this.

Link to comment
Share on other sites

I can really recommend http://adamwathan.me/refactoring-to-collections/ if you're curious (has a free sample as well). Brought a lot of light into all those functions and their usefulness. 

Read through the sample. It's nice and plainly put. I could definitely see that helping me. In fact I can tell there are a lot of lessons in there that would improve the way I want to build this site, and it would be better to implement them here so close to the start. Not quite sure I can get that price approved, though.

At least it gives me some hints about where I should start looking.

Definitely sounds like arrays are not something I should be actively avoiding in favor of foreach loops!

I wasn't able to get your code sample working before I quit for the day, but I'll be back at it in the morning.

Thanks again!

Link to comment
Share on other sites

Ok, slightly out of context, because it's Javascript, but these 3 videos from funfunfunction could really help you to understand those 3 functions:

It's a series, watch them in sequence

  • Like 7
Link to comment
Share on other sites

$byColor = array_reduce($children->getArray(), function($carry, $child){
  $carry[$child->color][] = $child;
  return $carry;
}, array());

$toTextBlocks = array_map(function($color, $children){
  $childrenPA = (new PageArray)->import($children);
  return "$color (" . $children->implode(", ", "title") . ")";
}, array_keys($byColor), $byColor);

$line = implode(", ", $toTextBlocks);

I receive an illegal offset type error on  the following

 $byProd_fiber_type = array_reduce($children->getArray(), function($carry, $child){
  $carry[$child->prod_fiber_type][] = $child; // ** error reported here **
  return $carry;
  echo count( $carry );
}, array());

$toTextBlocks = array_map(function($title, $children){
  $childrenPA = (new PageArray)->import($children);
  return "$prod_fiber_type (" . $children->implode(", ", "prod_fiber_type") . ")";
}, array_keys($byProd_fiber_type), $byProd_fiber_type);

$line = implode(", ", $toTextBlocks);
 

Note I've changed "color" to "prod_fiber_type" since that was just a for instance (and I needed it to match a field to avoid a non-object error on the implode).

Link to comment
Share on other sites

Make sure $child->prod_fiber_type is a string and not an object (page field?). 

Ah yes, derp, thank you. I chose a bad field to start with. Field type Options, that one.

Now here is my code (I'll sort out converting array values to strings later), using a text field. I get a call to a non object for the implode at line 9.

$byPrice_emea_eur = array_reduce($children->getArray(), function($carry, $child){
	  $carry[$child->price_emea_eur][] = $child;
	  return $carry;
	  echo count( $carry );
	}, array());

$toTextBlocks = array_map(function($price_emea_eur, $children){
  $childrenPA = (new PageArray)->import($children);
  return "$price_emea_eur (" . $children->implode(", ", "price_emea_eur") . ")";
}, array_keys($byPrice_emea_eur), $byPrice_emea_eur);

$line = implode(", ", $toTextBlocks);

Link to comment
Share on other sites

Ah yes, derp, thank you. I chose a bad field to start with. Field type Options, that one.

Not bad at all, you just need to use $child->field->title (or ->value, if you're using those). It's just that the key needs to be some sting or int.

And replace line 9 with this. Missed the PA part of the variable. Also the second argument of the implode call is supposed to be the "title" of your children or you'll just end up with something like this: green (green, green, green), blue (blue, blue)

return "$price_emea_eur (" . $childrenPA->implode(", ", "title") . ")";
Link to comment
Share on other sites

Not bad at all, you just need to use $child->field->title (or ->value, if you're using those). It's just that the key needs to be some sting or int.

And replace line 9 with this. Missed the PA part of the variable. Also the second argument of the implode call is supposed to be the "title" of your children or you'll just end up with something like this: green (green, green, green), blue (blue, blue)

return "$price_emea_eur (" . $childrenPA->implode(", ", "title") . ")";

Thanks! that PA bit must have been what threw me when I just tried to grab ->title for the array, too.

It's working now! So I have two pages with fields with different values, $list gives me XXX.XX (Page A), XXX.ZZ (Page B) - Perfect!

When I change both pages to have the same value, it outputs XXX.XX (Page A, Page B). Which would be great if there were a XXX.YY (Page C). But when all (no matter the count 1 - 100) have the same value, I want to suppress the bit in the parenthesis. 

I imagine I'd build an if statement that compared the values before deciding to build $toTextBlocks? But I don't quite understand where the contents are being compared now. Before line 5? Something to do with $carry?

I promise I'm also trying to look this up on php.net as I go, but I'm a babe in the woods on this one.

Link to comment
Share on other sites

Ok, slightly out of context, because it's Javascript, but ...

Fun series, but every time someone blurts out something like "shorter code is better", I get the feeling that they've never heard of Perl. Granted he did claim that this is almost always true, but still :)

Say hello to this actual solution to a round of Perl golf back from '02: $_=pop;s#.#push@;,$;until$;++&$;;$@[$_]^=$;/$_&$&for@;,$;#eg;print@@,$/.

Link to comment
Share on other sites

Essentially we're doing this in the array_reduce()

[Page, Page, Page]

=>

[
  'key' => [Page, Page],
  'key2' => [Page]
]

So just count how many items the resulting array has. If 1 show only the key, otherwise do the array_map().

Fun series, but every time someone spurts out something like "shorter code is better", I get the feeling that they've never heard of Perl. Granted he did claim that this is almost always true, but still :)

I think the argument about more structure (compared to loops) is far more valid than the "shorter is better" one. I'm using Laravel's collection package via https://github.com/tightenco/collect with some additional hooked in functions and especially such transformation tasks are so much clearer to create as well as to read.

  • Like 1
Link to comment
Share on other sites

Essentially we're doing this in the array_reduce()

[Page, Page, Page]

=>

[
  'key' => [Page, Page],
  'key2' => [Page]
]

So just count how many items the resulting array has. If 1 show only the key, otherwise do the array_map().

I think the argument about more structure (compared to loops) is far more valid than the "shorter is better" one. I'm using Laravel's collection package via https://github.com/tightenco/collect with some additional hooked in functions and especially such transformation tasks are so much clearer to create as well as to read.

Ah, I understand now. Thank you!

Really, it bears repeating: thank you!

Here's the code that you helped me build:

 $byPrice_emea_eur = array_reduce($children->getArray(), function($carry, $child){
	  $carry[$child->price_emea_eur][] = $child;
	  return $carry;
	}, array());
	
	
 if(count($byPrice_emea_eur) > 1) {
		$toTextBlocks = array_map(function($price_emea_eur, $children){
		  $childrenPA = (new PageArray)->import($children);
		  return "€ $price_emea_eur (" . $childrenPA->implode(", ", "title") . ")";
		}, array_keys($byPrice_emea_eur), $byPrice_emea_eur);
		
		$line = implode(", ", $toTextBlocks);
	} else { 
		$line = "€" . $page->child->price_emea_eur;
	}
	echo "Price: " . $line . "";
Link to comment
Share on other sites

I think the argument about more structure (compared to loops) is far more valid than the "shorter is better" one. I'm using Laravel's collection package via https://github.com/tightenco/collect with some additional hooked in functions and especially such transformation tasks are so much clearer to create as well as to read.

Agreed, that makes a lot more sense. I'm one of those developers who tend to lean towards the simplest possible implementation and as few external dependencies as possible, even if that means writing a few extra loops, but I can definitely see the value in this :)

  • Like 1
Link to comment
Share on other sites

For the simple minded this would be the foreach version that does the same. ;)

$colors = array();
$childrenByColor = array();

foreach($children as $child) $colors[$child->color][] = $child;

foreach($colors as $color => $items) {
    $childrenByColor[] = "$color (" . $items->implode(",", "title") . ")";
}

$line = implode(", ", $childrenByColor);
  • Like 2
Link to comment
Share on other sites

For the simple minded this would be the foreach version that does the same. ;)

$colors = array();
$childrenByColor = array();

foreach($children as $child) $colors[$child->color][] = $child;

foreach($colors as $color => $items) {
    $childrenByColor[] = "$color (" . $items->implode(",", "title") . ")";
}

$line = implode(", ", $childrenByColor);

Which is closer to how I might have naturally gone, but once I really get into the hundreds of lines for each Series page, won't I be better off with something more like what we developed above? As I'm still a beginner I constantly worry that I could be writing more efficient code, and now with the scale of this site being as large as it is, in terms of fields per product, and with all the field values I have to consider from isset/true-false/count and so forth, I want to build this with a solid foundation.

Actually what I figure I really ought to do is double down and create a function for an include that I can call per field (there might be two or three types, depending on the data), to make that 'shorter is better' standard apply, at least to my template page. First I just want to get it outputting as intended, though.

Now I'm trying to sort out my logic in the event that one of the children has a blank field (a blank field still creates a key meaning (count($byPrice_emea_eur) > 1) is still triggered).

Link to comment
Share on other sites

Further down the rabbit hole..

I'm still getting an error at line 128 so anything beyond that I haven't discovered yet. The error I'm getting is that $specification isn't defined (I expected it to be held as part of the key/value pair). Is that because we're within the brackets of the foreach that builds the array?

	$children = $page->children;
	$iterationNum = count( $children ); 
		echo $iterationNum . " model";
		if($iterationNum > 1) {echo "s";}
		echo " in this series";
		
	foreach( $children as $key => $model ) { // numbers the children starting from 0, and $model becomes their page ID
		echo "<ul>";
        foreach( $model as $specification => $specvalue ) { // each child becomes an array containing the field names and values
            if($specvalue != '') {
	            $bySpec = array_reduce($children->getArray(), function($carry, $child){
		            $value = $child->get($specification);
	  $carry[$value][] = $child;
	  return $carry;
	  echo count( $carry );
	}, array());
 
	$toTextBlocks = array_map(function($field, $children){
	  $childrenPA = (new PageArray)->import($children);
	  return "$field (" . $children->implode(", ", "title") . ")";
	}, array_keys($bySpec), $bySpec);
	 
	$line = implode(", ", $toTextBlocks);
            }
    }

To describe what I'm trying to do: I want to cycle through the children of a page (sometimes it's going to be one. Typical is 2 - 8. There's one product series that includes over 180 possible items, though), and find the pertinent specs (the ones with values). I want to see which specs are the same for all products (I can't wait for pages to inherit values from a parent!) and then display those as simply as possible. Where a child under the series page doesn't have a value for a field that a sibling does, I want to indicate that one is an exception (or if only one does, that is listed as "(XXXX only)".

Most of my fields are text fields, but some are integers, others are options, and occasionally we have one that's a Page type, or an image.

I guess I would also need to figure out a way to omit some of these fields intentionally (f.ex. ones that will be addressed separately, or I want to keep hidden).

At the bottom of the page, I plan to also display a table for each child in the series, with a sort of cross comparison of only those specs that differ.

Am I totally off my nut to try and build this in this manner?

Link to comment
Share on other sites

For the simple minded this would be the foreach version that does the same. ;)

$colors = array();
$childrenByColor = array();

foreach($children as $child) $colors[$child->color][] = $child;

foreach($colors as $color => $items) {
    $childrenByColor[] = "$color (" . $items->implode(",", "title") . ")";
}

$line = implode(", ", $childrenByColor);

 I'm all for simple!! Have a good one. I think these quotes are fairly well known among coders... not sure... thought I'd share them anyway . Good stuff. Enjoy. :)

Correct is better than fast. Simple is better than complex. Clear is better than cute. Safe is better than insecure.

-- Sutter and Alexandrescu, C++ Coding Standards

Programs must be written for people to read, and only incidentally for machines to execute.

-- Harold Abelson and Gerald Jay Sussman

The cheapest, fastest and most reliable components of a computer system are those that aren't there.

-- Gordon Bell

Link to comment
Share on other sites

  • 1 year later...

I'm gnawing on this bone again, folks.

I was trying to do something else, iterating through fields in a page, and realized I had half of what I needed already working from this thread.

But tying them together is giving me trouble, which I think is in regard to the way the API and php are working together.

		$children = $page->children;
		$fields = $children->first()->template->fields;
		echo "<table width=100% border=1><tr><th><strong>Field (fieldType)</strong></th><th><strong>Field Value (children that share the value)</strong></th></tr>";
		foreach($fields as $f) { // fields first
			if($f->type != FieldtypeFieldsetTabOpen|FieldtypeFieldsetClose){
			echo "<td valign=top align=left>{$f->label}<br />({$f->type})</td>";
						
				$byField = array_reduce($children->getArray(), function($carry, $child){
				  $carry[$child->$f][] = $child; // HERE IS MY ISSUE 
				  return $carry;
				  echo count( $carry );
				}, array());
			
			$toTextBlocks = array_map(function($field, $children){
			  $childrenPA = (new PageArray)->import($children);
			  return "$field (" . $childrenPA->implode(", ", "title") . ")";
			}, array_keys($byField), $byField);
			
			$line = implode(", ", $toTextBlocks);
	
			echo "<td valign=top align=left>". $line . "</td></tr>";
			}
		}
		echo "</table>";

So in the line commented of note, there seems to be a disconnect between $child->$f and my intended $child->field_name.

 

TracyDebugger says $f is undefined at this point, but we are inside the loop where $f should be defined. I'm at a loss at the moment.

I'm working on this as well but I'm reaching out in the hopes one way or another I find the solution today.

 

Thanks in advance!

Edited by creativejay
TracyDebugger gave me half the answer
Link to comment
Share on other sites

4 minutes ago, flydev said:

What is going on if you call it that way (minus $) ? :


$carry[$child->f][] = $child; // no more ISSUE 

 

This clears the undefined variable error, but there's no output for the value of $field, nor even if I create a custom field called "f" and add a value.

In fact I'm noticing the code seems only to work if I use $child->headline or $child->title. Anything other static field name from this page outputs nothing.

This tangle is getting worse and worse.

Link to comment
Share on other sites

Ok got it, you need to allow the anonymous function to "capture" local variables. Try the following ( note the use($f) ) :

$byField = array_reduce($children->getArray(), function($carry, $child) use($f) {
				if(!is_object($child->$f) && !is_null($child->$f))  // avoid illegal offset type warning
					$carry[$child->$f][] = $child; // NO MORE ISSUE 
				return $carry;
				echo count( $carry );
		}, array());

 

  • Like 2
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

×
×
  • Create New...