Skip to main content

Konstantinos Loupasakis

When PHP garbage collector breaks your stuff

TL;DR

foreach($obj->getIterator() as $item){}

and

$iterator = $obj->getIterator();
foreach($iterator as $item){}

do not necessarily have the same behaviour.

A colleague of mine has been working on a project that uses LDAP. Everything was fine and great until he deployed the application to production he started receiving this error message:

Could not rewind entries array

So when he showed the issue to the rest of the team we did what everyone in their right minds would do; start debugging the framework by adding die() statements here and there. After some hacking, we ended up inside this piece of code:

public function rewind()
{
    $this->current = ldap_first_entry($this->connection, $this->search);
    if (false === $this->current) {
        throw new LdapException(sprintf('Could not rewind entries array: %s', ldap_error($this->connection)));
    }
}

The funny thing was that while $this->current was FALSE and caused the function to throw an LdapException, ldap_error($this->connection) returned the string ‘Success’. Huh.

After spending too much time making changes and seeing what breaks, suddenly the application started working correctly, seemingly without any real changes to the code.

Here is the change that apparently fixed everything:

 /**
  * @return \Symfony\Component\Ldap\Adapter\CollectionInterface
  */
  public function getResults($query)
  {
      $this->ldap->bind($this->searchDn, $this->searchPassword);
          $search = $this->ldap->query($this->baseDn, $query);
      return $search->execute();
  }

 /**
  * BROKEN
  */
  public function getAllUsers()
  {
      foreach ($this->getResults($this->allUsersQuery) as $ldapUser) {
           $results[] = $this->extractUserInfo($ldapUser);
      }
      return $results;
  }

 /**
  * WORKING
  */
  public function getAllUsers()
  {
      $queryResults = $this->getResults($this->allUsersQuery);
      foreach ($queryResults as $ldapUser) {
           $results[] = $this->extractUserInfo($ldapUser);
      }
      return $results;
  }

Do you see the difference between these two functions? I certainly don’t! What I can see that in the first we iterate the returned CollectionInterface directly inside the for loop, and in the second we assign it to a variable first, but it shouldn’t have an effect, right?

Well spoiler alert, it does.

I’m not 100% sure, but I’m suspecting a garbage collector releasing the query result before the iterator gets a chance to rewind.

Must investigate more.