creativejay

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

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:

Share this post


Link to post
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

Share this post


Link to post
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.

Share this post


Link to post
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!

Share this post


Link to post
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

Share this post


Link to post
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).

Share this post


Link to post
Share on other sites

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

  • Like 1

Share this post


Link to post
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);

Share this post


Link to post
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") . ")";

Share this post


Link to post
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.

Share this post


Link to post
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@@,$/.

Share this post


Link to post
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

Share this post


Link to post
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 . "";

Share this post


Link to post
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

Share this post


Link to post
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

Share this post


Link to post
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).

Share this post


Link to post
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?

Share this post


Link to post
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

Share this post


Link to post
Share on other sites

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

Share this post


Link to post
Share on other sites

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

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

 

Share this post


Link to post
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.

Share this post


Link to post
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

Share this post


Link to post
Share on other sites

Thank you very much! That's working.

Next step: figure out how to output values other than integers and strings.

Share this post


Link to post
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

  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By louisstephens
      From my last post, I was given a good idea on how to count the repeater items, and it worked wonderfully. I got my code working well and the columns (based on the count) all work well as well. Now, I have a head scratcher on my hands. 
      <?php $buttonsIncluded = $page->special_custom_buttons->find('special_custom_buttons_include=1'); $buttonsIncludedCount = count($buttonsIncluded); $buttonsIncludedCountAdditional = $buttonsIncludedCount +1; echo $buttonsIncludedCount; ?> <div class="row"> <?php foreach($buttonsIncluded as $button): ?> <?php if($button->custom_buttons_include): ?> <?php if($buttonsIncludedCountAdditional == 2): ?> <div class="col-6"> <a href=""><?php echo $button->custom_buttons_text; ?></a> </div> <?php elseif($buttonsIncludedCountAdditional == 3): ?> <div class="col-4"> <a href=""><?php echo $button->custom_buttons_text; ?></a> </div> <?php elseif($buttonsIncludedCountAdditional == 4): ?> <div class="col-3"> <a href=""><?php echo $button->custom_buttons_text; ?></a> </div> <?php endif; ?> <?php endif; ?> <?php endforeach; ?> </div> All of this is included in a larger foreach statement that is pulling in other data (like body copy etc etc) from a Page Table field. As you can see in my code above, I am adding "1" to the count, so I can have space in the grid layout for a new button.
      So, right now: it looks something like: 
      [repeater button] [repeater button] [repeater button] [space for new button] What I really need to do is to pull in the button from the Page Table and add it into the new space so it looks like:
      [repeater button] [repeater button] [repeater button] [button from Page Table] Is this even possible todo, or is there a better way to go about this? 
       
      *Edit*
      So, I really just overlooked something quite easy here. Since the grid is based on 12 columns, I could just take 12 and divide by $buttonsIncludedCountAdditional which would give me the remaining col width to use outside the foreach loop. I was trying to make this too complicated.
    • By rolisx
      Hi all
      I have a problem here. I created a gallery with 240 pictures. Created an images field with no maximum amount (0). Unfortunately, only 98 of the 240 images show on the website. Any idea what I possibly could have done wrong? Thanks for your help!
      <?php foreach ($page->images as $image) { $options = array( 'quality' => 90, 'upscaling' => false ); $thumb = $image->size(250, 250, $options); ?> <div class="col-xl-2 col-lg-3 col-md-4 col-sm-6 col-6 foto"> <a href="<?= $image->url ?>" data-lightbox="lightbox" > <img src="<?= $thumb->url ?>" alt="" > </a> </div> <?php } ?>  
    • By celfred
      Hello !
      I have somehting I don't understand here... Here's my code :
      $allPlayers = $pages->find("parent.name=players, team=$selectedTeam"); $allTrains = $allPlayers->find("template=event, task.name~=ut-action, refPage!='', date>=$startDate, date<=$endDate, sort=refPage, sort=date"); bd('$allTrains:'.$allTrains->count()); // DISPLAYS 0 ???? foreach($allPlayers as $p) { $allTrainings = $p->find("template=event, task.name~=ut-action, refPage!='', date>=$startDate, date<=$endDate, sort=refPage, sort=date"); $test += $allTrainings->count(); } bd('$test:'.$test); // DISPLAYS 883 pages (normal) As you can read from my comments, I have no idea why my first $allTrains stays at 0 while the second request actually finds the corresponding pages. If someone could explain I'd appreciate a lot. I have been struggling with this for hours now... For your information, my pages having template 'event' are in a subtree like so :
      - player 01
        - history-1
          - event 01
         - event 02
         - event ...
        - history-2
          - event 01
          - event ...
      - player 02
        - history-1
          - event 01
         - event 02
         - event ...
        - history-2
          - event 01
          - event ...
      - player ...
        - history-1
          - event 01
         - event 02
         - event ...
        - history-2
          - event 01
          - event ...
       
      Thanks in advance. (sorry for my preceding 'tree' which doesn't look like much. I need to find a way to output this better 😉 )
    • By Sten
      Hi,
      I have a lot of difficulties with getting a field value.
      First I created a template, inside I created several fields
      two fields are a dropdown select box So I have a value and a label for each line.
      Then I want to get the field value in _init.php or in the template to display it. How can I get the value of a field for the page ? I need to understand the flow through which data goes.
      Could someone help me ?
      Thank you
       
       
       
    • By burning
      Hi All, maybe a really stupid question, but is there a way to sort repeater items on -created?
      Like $page->social_activity('limit=10,sort=created'), with 'social_activity' as the repeater field?
      Tried this but it didnt work 😞 
      Any help appreciated!