PHP Variable Scope and "foreach" - php

My application is building PDF documents. It uses scripts to produce each page's HTML.
The PDF-Generating class is "Production", and page class is "Page".
class Production
{
private $_pages; // an array of "Page" objects that the document is composed of
public getPages()
{
return $this->_pages;
}
public render()
{
foreach($this->_pages as $page) {
$pageHtml = $page->getHtml($this); // Page takes a pointer to production to access some of its data.
}
}
}
Here is the Page class summary:
class Page
{
private $scriptPath; // Path to Script File (PHP)
public function getHtml(Production &$production)
{
$view = new Zend_View();
$view->production = $production;
return $view->render($this->scriptPath);
}
}
I've encountered a problem when coding Table of Contents. It accesses Production, get all the pages, queries them, and builds TOC based on page titles:
// TableOfContents.php
// "$this" refers to Zend_View from Pages->getHtml();
$pages = $this->production->getPages();
foreach($pages as $page) {
// Populate TOC
// ...
// ...
}
What happens is that foreach inside the TableOfContents.php is interfering with foreach in Production. Production foreach loop is terminated at Index page (which is actually a second page in the document, after the cover page).
The Document Layout is like so:
1) Cover Page
2) Table of Contents
3) Page A
4) Page B
5) Page C
TableOfContents.php, in its foreach loop, goes through the pages as required and builds an index of the entire document, but the loop in Production terminates at Table of Contents and does not proceed to render Pages A, B and C.
If I remove foreach from TableOfContents.php, all consecutive pages are rendered appropriately.
I feel it's a problem with the pointer and variable scope, so what can I do to fix it?

Diagnosis
I suspect the problem is that $_pages isn't a normal PHP array, but instead an object which happens to implement the Iterator interface. Because of this, the "state" of the foreach loop is stored on the object itself, which means that the two loops are conflicting.
If $_pages was a plain array, then there would be no problem, since the line $pages = $this->production->getPages(); would make a copy, since PHP arrays are copied on assignment (unlike objects) and also because nested foreach loops on a normal array don't have that problem. (Presumably from some internal array copy/assignemnt logic.)
Solution
The "fast and dirty" fix is to avoid foreach loops, but I think that would be both annoying and be a cause of future bugs because it's very easy to forget that $_pages needs super-special treatment.
For a real fix, I suggest looking at whatever class is behind the object in $_pages and see if you can change that class. Instead of having $_pages be the Iterator, change $_pages so that it provides iterators through the IteratorAggregate interface.
That way every foreach loop asks for a separate iterator object and maintains separate state.
Here is a sample script to illustrate the problem, partially cribbed from the PHP reference pages:
<?php
class MyIterator implements Iterator
{
private $var = array();
public function __construct($array)
{
if (is_array($array)) {
$this->var = $array;
}
}
public function rewind()
{
reset($this->var);
}
public function current()
{
$var = current($this->var);
return $var;
}
public function key()
{
$var = key($this->var);
return $var;
}
public function next()
{
$var = next($this->var);
return $var;
}
public function valid()
{
$key = key($this->var);
$var = ($key !== NULL && $key !== FALSE);
return $var;
}
}
// END BOILERPLATE DEFINITION OF ITERATOR, START OF INTERESTING PART
function getMyArrayThingy(){
/*
* Hey, let's conveniently give them an object that
* behaves like an array. It'll be convenient!
* Nothing could possibly go wrong, right?
*/
return new MyIterator(array("a","b","c"));
}
// $arr = array("a,b,c"); // This is old code. It worked fine. Now we'll use the new convenient thing!
$arr = getMyArrayThingy();
// We expect this code to output nine lines, showing all combinations of a,b,c
foreach($arr as $item){
foreach($arr as $item2){
echo("$item, $item2\n");
}
}
/*
* Oh no! It printed only a,a and a,b and a,c!
* The outer loop exited too early because the counter
* was set to C from the inner loop.
*/

I'm not sure to understand what is your problem, but you may look at the PHP function reset =)

The solution was the avoid using foreach and use conventional loops, as suggested here:
nested foreach in PHP problem

Related

unsetting objects in an array based on a bool method

I'm trying to filter an array of objects implementing a specific interface (which simply defines the isComplete(): bool method) based on the result of that method. array_filter doesn't work because it can't call a method on each object to determine whether to filter it (or can it?). I've tried writing a function that takes the splatted array as an argument by reference, this doesn't work either:
function skipIncomplete(CompletableObjectInterface &...$objects): array {
$skipped = [];
foreach ($objects as $index => $item) {
if (!$item->isComplete()) {
$skipped[] = $item->id ?? $index;
unset($objects[$index]);
}
}
return $skipped;
}
The original elements passed in simply don't end up getting unset.
I'm looking for a way that doesn't include creating an entirely new Collection class to hold my CompletableObjects for complexity reasons. I really want to keep the type hint so no one can pass in a generic array, causing runtime errors when the function tries to call $item->isComplete.
Is there any way I can achieve this in PHP 7.3.15?
Added a filter, please comment as to what is wrong with this type of approach:
<?php
interface CompletableObjectInterface {
public function isComplete() : bool;
}
class Foo implements CompletableObjectInterface
{
public function isComplete() : bool
{
return false;
}
}
class Bar implements CompletableObjectInterface
{
public function isComplete() : bool
{
return true;
}
}
$foo = new Foo;
$bar = new Bar;
$incomplete = array_filter([$foo, $bar], function($obj) { return !$obj->isComplete();});
var_dump($incomplete);
Output:
array(1) {
[0]=>
object(Foo)#1 (0) {
}
}
Looks like you got a bit hung up on a wrong understanding of the ... syntax for a variable number of arguments.
You are passing in one array, and the $objects parameter will therefore contain that array in the first index, i.e. in $objects[0].
So in theory you could just change your line
unset($objects[$index]);
to
unset($objects[0][$index]);
However, I do not really see why the variable number of arguments syntax is used at all, since you apparently are just expecting one array of values (objects in this case) as an argument to the function. Therefore I'd recommend you just remove the ... from the argument list and your solution does what you wanted.
Alternatively you can of course add an outer foreach-loop and iterate over all passed "arrays of objects", if that is an use case for you.

Class-Wide accessible static array, trying to push to it, but keeps coming back empty

class UpcomingEvents {
//Variable I'm trying to make accessible and modify throughout the class methods
private static $postObjArr = array();
private static $postIdArr = array();
private static $pinnedPost;
//My attempt at a get method to solve this issue, it did not
private static function getPostObjArr() {
$postObjArr = static::$postObjArr;
return $postObjArr;
}
private static function sortByDateProp($a, $b) {
$Adate = strtotime(get_field('event_date',$a->ID));
$Bdate = strtotime(get_field('event_date',$b->ID));
if ($Adate == $Bdate) {
return 0;
}
return ($Adate < $Bdate) ? -1 : 1;
}
private static function queryDatesAndSort($args) {
$postQuery = new WP_Query( $args );
if( $postQuery->have_posts() ) {
while( $postQuery->have_posts() ) {
$postQuery->the_post();
//Trying to push to the array, to no avail
array_push(static::getPostObjArr(), get_post());
}
}
//Trying to return the array after pushing to it, comes back empty
return(var_dump(static::getPostObjArr()));
//Trying to sort it
usort(static::getPostObjArr(), array(self,'sortByDateProp'));
foreach (static::getPostObjArr() as $key => $value) {
array_push(static::$postIdArr, $value->ID);
}
}
}
I'm trying to access $postObjArr within the class, and push to it with the queryDatesAndSort(); method. I've tried a couple of things, most recent being to use a get method for the variable. I don't want to make it global as it's bad practice I've heard. I've also tried passing by reference I.E
&static::$postObjArr;
But when it hits the vardump, it spits out an empty array. What would be the solution and best practice here? To allow the class methods to access and modify a single static array variable.
static::$postObjArr[] = get_post()
I didn't think it would of made a difference, but it worked. Can you explain to me why that worked but array.push(); Did not?
Arrays are always copy-on-write in PHP. If you assign an array to another variable, pass it into a function, or return it from a function, it's for all intents and purposes a different, new array. Modifying it does not modify the "original" array. If you want to pass an array around and continue to modify the original array, you'll have to use pass-by-reference everywhere. Meaning you will have to add a & everywhere you assign it to a different variable, pass it into a function, or return it from a function. If you forget your & anywhere, the reference is broken.
Since that's rather annoying to work with, you rarely use references in PHP and you either modify your arrays directly (static::$postObjArr), or you use objects (stdClass or a custom class) instead which can be passed around without breaking reference.

One class to call method from all other classes in directory

Sorry for a bit misleading title but I don't quite know how to ask that better.
I have a directory with classes doing same job but with different implementation. They all look like this:
class someClass {
private static $variable=12345;
public static function someTask($keyword){
...
return array();
}
private function pf() {...}
}
The methods are taking same arguments, and returning array of the same structure.
I'd like to create one class to be able to call selected classes form that folder (and maybe each of them) combine their result, sort it by some criteria end return.
I thought of strategy pattern, but as far as I know it goes like :
$obj = new Context();
$obj->setStrategy(new ConcreteStrategy1);
$obj->getStrategy()->task(); // => „Strategy 1”
$obj->setStrategy(new ConcreteStrategy2);
$obj->getStrategy()->task(); // => „Strategy 2”
Each time I want to call another class I have to change it manually. That will leave me using foreach on some array of classes (strategies) names. Is that the right way to go? Also please help me find better title for that question because that one is misleading.
Strategy pattern it's just an algorithm that we extracted and put into some method. Then we pass the algorithm we need into context and use it there.
If you want to have one class that processes different strategies and combine their result I think Visitor pattern will be better.
I come up with a working solution
class SearchEngine {
public $instances=array();
public function __construct() {
$inputSites = $this->importClasses(); //import all classes in directory
foreach ($inputSites as $site) {
$classname = 'API\\' . $site . "Api";
$startegyInstance = new $classname();
array_push($this->instances,$startegyInstance);
}
}
public function searchByTitle($keyword) {
$results = array();
for ($i=0; $i<sizeof($this->instances); $i++){
//this searchByTitle is not recursion
$next = $this->instances[$i]->searchByTitle($keyword);
$results=array_merge($results,$next);
}
return $results;
}
private function importClasses($classesToImport){
foreach ($classesToImport as $class) {
$imp="API/".$class."Api.php";
require_once ($imp);
}
}
}
(I cut less significant details)
In classes directory I have interface and classes.
This solution works. I have not enough experience to judge it optimal or good.

ArrayObject doesn't allow me to unset a value, while iterating over it

I've got this notice:
ArrayIterator::next(): Array was modified outside object and internal position is no longer valid in /var/www...
which is produced by this code, at the begining of the foreach loop. Together with the notice, the foreach loop starts iterating all over again. In other words, the internal position is reset whenever this thing happens. But acording to php manual, ArrayObject is using ArrayIterator by default.
And manual says this about ArrayIterator
This iterator allows to unset and modify values and keys while iterating over Arrays and Objects.
Am I missing something here? I found some bugreports about ArratIterator, but not this kind. Is it a bug or is it my bad?
version: PHP Version 5.3.10-1ubuntu3.4
<?php
//file 1:
// no namespace
abstract class holder extends \ArrayObject{
// abstract function init();
public function __construct($init){
parent::__construct($init, 1);
}
}?>
<?php
//file 2:
namespace troops;
class holder extends \holder{
public function __construct(){
parent::__construct($this->init());
}
private function init(){
return array( /*... some data from db ...*/ );
}
public function saveData(){
foreach($this as $k => $v){
$this->save($v);
if($v->number_of_items==0) {
unset($k);
// $this->offsetUnset($k); // tryed both
}
}
}
}
?>
ArrayObject implements IteratorAggregate which means it has a method called getIterator() which returns an iterator.
php's foreach loop will automatically retrieve an iterator by calling the getIterator() method for you to retrieve an iterator to iterate over. This is convenient, but you need to get a reference to this iterator in order to call the offsetUnset() method on the iterator itself. The key thing here is you must call the iterators offsetUnset() method, not the ArrayObjects offsetUnset() method.
$ao = new ArrayObject();
$ao[] = 9;
$iter = $ao->getIterator();
foreach ($iter as $k => $v)
$iter->offsetUnset($k); // no error
The underlying ArrayObject that the iterator is iterating over will be mutated, so if you have more than one active iterator at the same time over the same Arrayobject, you'll still encounter the same error.
The rationale for this is likely so that the iterators can be memory efficient and not have to copy the underlying ArrayObject, because a copy is the only simple solution to dealing with the complexity of deciding what the current iterator position should be when things are added to or deleted from the underlying array.
In case you remove the last record from array than the next foreach loop can fail (internaly calling $this->next() )
Forexample when i have array of one item and I will unset it, than next foreach fails.
So it helped me to test the validy and break in this case the next loop.
deleteOffset = true;
foreach ($iterator as $key => $value)
{
if ($deleteOffset && $iterator->offsetExists($key) )
{
$iterator->offsetUnset($key);
}
//if remove last record than the foreach ( $this->next() ) fails so
//we have to break in this case the next ->next call
//after any ->offsetUnset calls
if (!$iterator->valid()) break;
}
This took me so long! I used the recipe on this page to (unsuccessfully) clear-out the contents of an ArrayObject's storage, then I found this little diddy..
$this->exchangeArray(array());
magic!

php arrays with only one type

I'm looking to create an array or list with elements of a certain type (eg objects the implement a certain interface). I know I can create an object that does the same thing implementing Traversable and Iterator, or override ArrayObject. But maybe there's another way I have missed.
Do you mean something like:
$array=Array();
foreach ($itemsToAdd as $item) {
if ($item instanceof NameOfwantedInterface) {
Array_push($array,$item);
}
}
If you don't, them I'm sorry - it's just that your question isn't too clear.
I would write a custom class that extended ArrayObject and threw an exception if you tried to assign a variable that wasn't the correct type, there's really no better way to do it that I can think of.
PHP as a lanugage is very flexible in terms of type handling and type conversion. You will probably have to put a manual check in if you want any kind of strong type checking, a simple if statement will do.
The array object is designed to be especially flexible (lazy key assignment, automatic increment, string or integer keys, etc.) so you should probably use a custom object of your own.
You could use type hinting:
<?php
interface Shape
{
function draw();
}
class MyArray
{
private $array = array();
function addValue(Shape $shape) //The hinting happens here
{
$array[] = $shape;
}
}
?>
This example is not perfect, but you'll get the idea.
Basically, you are going to want to do a function that checks if the variable you are inserting into the array is an object.
function add($var)
{
if(is_object($var))
{
$this->array[] = $var;
}
}
If you want to make sure it has a specific class name, you would do something like this (for PHP5):
function add(className $var)
{
$this->array[] = $var;
}
or this for previous PHP versions:
function add($var)
{
if($var instanceOf className)
{
$this->array[] = $var
}
}
You might want to look into array_filter() to do this without building an object.
Looking at that page, I've found that you can use array_filter with common functions like is_object. So doing something like this:
$this->array = array_filter($this->array ,'is_object');
Would filter the array to contain only objects.

Categories