Here's something simple for someone to answer for me. I've tried searching but I don't know what I'm looking for really.
I have an array from a JSON string, in PHP, of cast and crew members for a movie.
Here I am pulling out only the people with the job name 'Actor'
foreach ($movies[0]->cast as $cast) {
if ($cast->job == 'Actor') {
echo '<p>' . $cast->name . ' - ' . $cast->character . '</p>';
}
}
The problem is, I would like to be able to limit how many people with the job name 'Actor' are pulled out. Say, the first 3.
So how would I pick only the first 3 of these people from this array?
OK - this is a bit of over-kill for this problem, but perhaps it serves some educational purposes. PHP comes with a set of iterators that may be used to abstract iteration over a given set of items.
class ActorIterator extends FilterIterator {
public function accept() {
return $this->current()->job == 'Actor';
}
}
$maxCount = 3;
$actors = new LimitIterator(
new ActorIterator(
new ArrayIterator($movies[0]->cast)
),
0,
$maxCount
);
foreach ($actors as $actor) {
echo /*... */;
}
By extending the abstract class FilterIterator we are able to define a filter that returns only the actors from the given list. LimitIterator allows you to limit the iteration to a given set and the ArrayIterator is a simple helper to make native arrays compatible with the Iterator interface. Iterators allow the developer to build chains that define the iteration process which makes them extremely flexible and powerful.
As I said in the introduction: the given problem can be solved easily without this Iterator stuff, but it provides the developer with some extended options and enables code-reuse. You could, for example, enhance the ActorIterator to some CastIterator that allows you to pass the cast type to filter for in the constructor.
Use a variable called $num_actors to track how many you've already counted, and break out of the loop once you get to 3.
$num_actors = 0;
foreach ( $movies[0]->cast as $cast ) {
if ( $cast->job == 'Actor' ) {
echo '...';
$num_actors += 1;
if ( $num_actors == 3 )
break;
}
}
$actors=array_filter($movies[0]->cast, function ($v) {
return $v->job == 'Actor';
});
$first3=array_slice($actors, 0, 3);
or even
$limit=3;
$actors=array_filter($movies[0]->cast, function ($v) use (&$limit) {
if ($limit>0 && $v->job == 'Actor') {
$limit--;
return true;
}
return false;
});
Add a counter and an if statement.
$count = 0;
foreach ($movies[0]->cast as $cast)
{
if ($cast->job == 'Actor')
{
echo '<p>' . $cast->name . ' - ' . $cast-character . '</p>';
if($count++ >= 3)
break;
}
}
$limit = 3;
$count = 0;
foreach ($movies[0]->cast as $cast) {
// You can move the code up here if all you're getting is Actors
if ($cast->job == 'Actor') {
if ($count == $limit) break;// stop the loop
if ($count == $limit) continue;// OR move to next item in loop
$count++;
echo '<p><a href="people.php?id='
. $cast->id
. '">'
. $cast->name
. ' - '
. $cast->character
. '</a></p>';
}
}
Related
I'm generating an XML document with PHP and the elements have to be in a specific order. Most of them work fine, with the exception of three. The child elements looks like this:
<p>
<co>ABC</co>
<nbr>123456</nbr>
<name>short product description</name>
<desc>long product description</desc>
<kw>searchable keywords</kw>
<i-std>relative/path/to/image</i-std>
<i-lg>relative/path/to/large/image</i-lg>
<i-thmb>relative/path/to/thumbnail</i-thmb>
<mfg>manufacturer</mfg>
<a-pckCont>package contents</a-pckCont>
</p>
The code I'm using works fine, but the three image elements are out of order, which makes the content processor that consumes them choke. What I've tried most lately is this:
$newStd = 0;
foreach ($items as $row => $innerArray) {
$p = $domTree->createElement('p');
$xmlRoot->appendChild($p);
foreach ($innerArray as $innerRow => $value) {
if ($innerRow != 'key') {
if ($value != '') {
echo $innerRow . ' : ' . $value . '<br />';
if ($innerRow == 'i-std') {
$newStd = $domTree->createElement($innerRow, htmlspecialchars($value));
} else {
$p->appendChild($domTree->createElement($innerRow, htmlspecialchars($value)));
}
}
}
if ($newStd != 0) {
$thmb = $p->getElementsByTagName('i-thmb')->item(0);
$p->insertBefore($newStd, $thmb);
}
}
}
My thought was to have it write out all child elements before I have it write the element in using InsertBefore to ensure it appears before the i-thmb element, but it didn't make a difference. No matter what I do, the output I get has them in the order i-thmb, i-std, i-lg. All other elements appear in the proper order, after rearranging some of the variables in the arrays used to build the XML document. I haven't attempted to control the i-lg element yet, since i-std isn't working.
Ultimately, this will be used to combine to XML documents together, but in testing to be sure the XML processor wasn't going to choke, I found the fundamental issue is that the order of elements largely determines if it will work or not (the system I'm working with is undocumented and support is poor, to say the least).
Edit to add: echoing as I do in the inner foreach loop shows them in the correct order but they're out of order in the output file.
I think a couple of changes would make the code more robust (I've added comments to the code for specifics). This mainly involves checking of data types and resetting the replacement field in each loop...
foreach ($items as $row => $innerArray) {
$p = $domTree->createElement('p');
$xmlRoot->appendChild($p);
foreach ($innerArray as $innerRow => $value) {
$newStd = 0; // Make sure this is set each time
if ($innerRow != 'key') {
if ($value != '') {
echo $innerRow . ' : ' . $value . '<br />';
if ($innerRow == 'i-std') {
$newStd = $domTree->createElement($innerRow, htmlspecialchars($value));
} else {
$p->appendChild($domTree->createElement($innerRow, htmlspecialchars($value)));
}
}
}
// Check if a replacement element, checking type of element
if ($newStd instanceof DOMElement) {
$thmb = $p->getElementsByTagName('i-thmb')->item(0);
$p->insertBefore($newStd, $thmb);
}
}
}
I have this code:
<?php
function generator() {
yield 'First value';
for ($i = 1; $i <= 3; $i++) {
yield $i;
}
}
$gen = generator();
$first = $gen->current();
echo $first . '<br/>';
//$gen->next();
foreach ($gen as $value) {
echo $value . '<br/>';
}
This outputs:
First value
First value
1
2
3
I need the 'First value' to yielding only once. If i uncomment $gen->next() line, fatal error occured:
Fatal error: Uncaught exception 'Exception' with message 'Cannot rewind a generator that was already run'
How can I solve this?
The problem is that the foreach try to reset (rewind) the Generator. But rewind() throws an exception if the generator is currently after the first yield.
So you should avoid the foreach and use a while instead
$gen = generator();
$first = $gen->current();
echo $first . '<br/>';
$gen->next();
while ($gen->valid()) {
echo $gen->current() . '<br/>';
$gen->next();
}
chumkiu's answer is correct. Some additional ideas.
Proposal 0: remaining() decorator.
(This is the latest version I am adding here, but possibly the best)
PHP 7+:
function remaining(\Generator $generator) {
yield from $generator;
}
PHP 5.5+ < 7:
function remaining(\Generator $generator) {
for (; $generator->valid(); $generator->next()) {
yield $generator->current();
}
}
Usage (all PHP versions):
function foo() {
for ($i = 0; $i < 5; ++$i) {
yield $i;
}
}
$gen = foo();
if (!$gen->valid()) {
// Not even the first item exists.
return;
}
$first = $gen->current();
$gen->next();
$values = [];
foreach (remaining($gen) as $value) {
$values[] = $value;
}
There might be some indirection overhead. But semantically this is quite elegant I think.
Proposal 1: for() instead of while().
As a nice syntactic alternative, I propose using for() instead of while() to reduce clutter from the ->next() call and the initialization.
Simple version, without your initial value:
for ($gen = generator(); $gen->valid(); $gen->next()) {
echo $gen->current();
}
With the initial value:
$gen = generator();
if (!$gen->valid()) {
echo "Not even the first value exists.<br/>";
return;
}
$first = $gen->current();
echo $first . '<br/>';
$gen->next();
for (; $gen->valid(); $gen->next()) {
echo $gen->current() . '<br/>';
}
You could put the first $gen->next() into the for() statement, but I don't think this would add much readability.
A little benchmark I did locally (with PHP 5.6) showed that this version with for() or while() with explicit calls to ->next(), current() etc are slower than the implicit version with foreach(generator() as $value).
Proposal 2: Offset parameter in the generator() function
This only works if you have control over the generator function.
function generator($offset = 0) {
if ($offset <= 0) {
yield 'First value';
$offset = 1;
}
for ($i = $offset; $i <= 3; $i++) {
yield $i;
}
}
foreach (generator() as $firstValue) {
print "First: " . $firstValue . "\n";
break;
}
foreach (generator(1) as value) {
print $value . "\n";
}
This would mean that any initialization would run twice. Maybe not desirable.
Also it allows calls like generator(9999) with really high skip numbers. E.g. someone could use this to process the generator sequence in chunks. But starting from 0 each time and then skipping a huge number of items seems really a bad idea performance-wise. E.g. if the data is coming from a file, and skipping means to read + ignore the first 9999 lines of the file.
solutions provided here does not work if you need to iterate more than once.
so I used iterator_to_array function to convert it to array;
$items = iterator_to_array($items);
I was looking at implementing the recursive function from this SO item: PHP: nested menu with a recursive function, expand only some nodes (not all the tree) to build a right click menu on my application and need to get the nested level. I am trying to think through the workflow here but can't think of where to put my increment by one and reset it to end up with variables such as:
$nestedLevel = 1; // main item
$nestedLevel = 2; // sub menu
$nestedLevel = 2; // sub-sub menu
I would like to use that to setup some if statements to I can handle each level differently. the Main level will be a simple list, Level 2 would be a horizontal button group, and the third will be simple lists assigned to tabs from the button group. Here is what I have:
function recursive($parent, $array) {
$has_children = false;
foreach($array as $key => $value) {
if ($value['menu_parent_id'] == $parent) {
if ($has_children === false && $parent) {
$has_children = true;
echo '<ul>' ."\n";
}
echo '<li>' . "\n";
echo '' . $value['name'] . '' . " \n";
echo "\n";
recursive($key, $array);
echo "</li>\n";
}
}
if ($has_children === true && $parent) echo "</ul>\n";
}
In the end it will look something like this (with some more styling of course):
I am trying to import a tab delimited file after upload. The meat of this is done with the following function. I'm trying to build an array of class instances. The code follows:
Import Function
$AddedProducts;
function importList($filename)
{
global $AddedProducts;
$AddedProducts=array();
$fileHandle = fopen($filename, "r");
$currentProduct = new productImport();
$line=fgets($fileHandle); $line=fgets($fileHandle); //throw away top 2 lines
echo '<hr>';
while(true)
{
$line = fgets($fileHandle);
if($line == null) break;
$cells=explode(' ', $line);
$i=0;
foreach($currentProduct as $ProductProperty)
{
if(isset($cells[$i]))
{
$ProductProperty = $cells[$i];
echo $i . '. ' . $cells[$i] . "<br>";
}
else return false;
$i++;
}
echo "<hr>";
$AddedProducts[]=$currentProduct;
}
fclose($fileHandle);
return true;
}
Array Output
<?
$i=0;
foreach($AddedProducts as $AddedProduct)
{
$i++;
echo "<hr>" . $i . "<br>";
foreach($AddedProduct as $key=>$value)
{
echo $key . ' = ' . $value . '<br>';
}
}
?>
Breakdown of Known Info
The final array length/size is correct. (Should be lines in file - 2)
It doesn't particularly matter how many properties are in the productImport class so long as it equates to the same number of tabs per line in the file being read.
importList function echos proper values for $cells[$i] which are the same values I'm missing in the array output.
The problem seems to be either the values aren't being assigned to the properties or the properties are not being read. I'm not sure of why either would be the case but I assume it is because PHP is not my primary language and likely something obvious about the foreach loops ;)
I'm using PHP v5.2.6
What's wrong with this code?
Answer:
foreach($currentProduct as $ProductProperty) becomes
foreach($currentProduct as &$ProductProperty)
I think the problem is in this section:
foreach($currentProduct as $ProductProperty)
{
if(isset($cells[$i]))
{
$ProductProperty = $cells[$i]; /* this seems to be the problem */
echo $i . '. ' . $cells[$i] . "<br>";
}
else return false;
$i++;
}
According to the php manual, Unless the array is referenced, foreach operates on a copy of the specified array and not the array itself. so the value you assign is discarded after the loop.
Edit: Apart from that, you are looping through object properties and although the manual does not explicitly state it, it seems you need foreach($class as $key => $value) instead of just foreach($class as $value)
In your foreach loops, the assigned variables such as $ProductProperty are not references, therefore they will not actually affect anything outside the loop.
i.e. $ProductProperty = $cells[$i] only affects the current iteration.
In addition to what the others are saying, it seems that you are attempting to insert property data to the same object every time, since you're not creating any new productImport instances in the loop.
I have the following PHP code which works out the possible combinations from a set of arrays:
function showCombinations($string, $traits, $i){
if($i >= count($traits)){
echo trim($string) . '<br>';
}else{
foreach($traits[$i] as $trait){
showCombinations("$string$trait", $traits, $i + 1);
}
}
}
$traits = array(
array('1','2'),
array('1','2','3'),
array('1','2','3')
);
showCombinations('', $traits, 0);
However, my problem is that I need to store the results in an array for processing later rather than just print them out but I can't see how this can be done without using a global variable.
Does anyone know of an alternative way to achieve something similar or modify this to give me results I can use?
Return them. Make showCombinations() return a list of items. In the first case you only return one item, in the other recursive case you return a list with all the returned lists merged. For example:
function showCombinations(...) {
$result = array();
if (...) {
$result[] = $item;
}
else {
foreach (...) {
$result = array_merge($result, showCombinations(...));
}
}
return $result;
}
In addition to the other answers, you could pass the address of an array around inside your function, but honestly this isn't nearly the best way to do it.
Using the variable scope modifier static could work. Alternatively, you could employ references, but that's just one more variable to pass. This works with "return syntax".
function showCombinations($string, $traits, $i){
static $finalTraits;
if (!is_array($finalTraits)) {
$finalTraits = array();
}
if($i >= count($traits)){
//echo trim($string) . '<br>';
$finalTraits[] = $string;
} else {
foreach($traits[$i] as $trait){
showCombinations("$string$trait", $traits, $i + 1);
}
}
return $finalTraits;
}
$traits = array(
array('1','2'),
array('1','2','3'),
array('1','2','3')
);
echo join("<br>\n",showCombinations('', $traits, 0));
Of course, this will work as expected exactly once, before the static nature of the variable catches up with you. Therefore, this is probably a better solution:
function showCombinations($string, $traits, $i){
$finalTraits = array();
if($i >= count($traits)){
$finalTraits[] = $string;
} else {
foreach($traits[$i] as $trait){
$finalTraits = array_merge(
$finalTraits,
showCombinations("$string$trait", $traits, $i + 1)
);
}
}
return $finalTraits;
}
although the solution by Lukáš is the purest as it has no side effects, it may be ineffective on large inputs, because it forces the engine to constantly generate new arrays. There are two more ways that seem to be less memory-consuming
have a results array passed by reference and replace the echo call with $result[]=
(preferred) wrap the whole story into a class and use $this->result when appropriate
the class approach is especially nice when used together with php iterators
public function pageslug_genrator($slug,$cat){
$page_check=$this->ci->cms_model->show_page($slug);
if($page_check[0]->page_parents != 0 ){
$page_checks=$this->ci->page_model->page_list($page_check[0]->page_parents);
$cat[]=$page_checks['re_page'][0]->page_slug;
$this->pageslug_genrator($page_checks['re_page'][0]->page_slug,$cat);
}
else
{
return $cat;
}
}
this function doesnt return any value but when i m doing print_r $cat it re
store the results in a $_SESSION variable.