Asked  7 Months ago    Answers:  5   Viewed   63 times

EDIT: I realized the amount of text might be intimidating. The essence of this question:
How to implement ArrayAccess in a way that makes setting multidimensional values possible?

 


 

I am aware that this was discussed here already but I seem unable to implement the ArrayAccess interface correctly.

Basically, I've got a class to handle the app configuration with an array and implemented ArrayAccess. Retrieving values works fine, even values from nested keys ($port = $config['app']['port'];). Setting values works only for one-dimensional arrays, though: As soon as I try to (un)set a value (eg. the port in the previous example), i get the following error message:

Notice:  Indirect modification of overloaded element <object name> has no effect in <file> on <line>

Now the general opinion seems to be that the offsetGet() method has to return by reference (&offsetGet()). That, however, does not solve the problem and I'm afraid I don't know how to implement that method correctly - why is a getter method used to set a value? The php doc here is not really helpful either.

To directly replicate this (PHP 5.4-5.6), please find a sample code attached below:

<?php

class Config implements ArrayAccess
{

    private $data = array();

    public function __construct($data)
    {
        $this->data = $data;
    }


    /**
     * ArrayAccess Interface
     * 
     */
    public function offsetSet($offset, $value)
    {
        if (is_null($offset)) {
            $this->data[] = $value;
        } else {
            $this->data[$offset] = $value;
        }
    }

    public function &offsetGet($offset)
    {       
        return isset($this->data[$offset]) ? $this->data[$offset] : null;
    }

    public function offsetExists($offset)
    {
        return isset($this->data[$offset]);
    }

    public function offsetUnset($offset)
    {
        unset($this->data[$offset]);
    }
}

$conf = new Config(array('a' => 'foo', 'b' => 'bar', 'c' => array('sub' => 'baz')));
$conf['c']['sub'] = 'notbaz';

 


 

EDIT 2: The solution, as Ryan pointed out, was to use ArrayObject instead (which already implements ArrayAccess, Countable and IteratorAggregate).
To apply it to a class holding an array, structure it like so:

<?php

class Config extends ArrayObject
{

    private $data = array();

    public function __construct($data)
    {
        $this->data = $data;
        parent::__construct($this->data);
    }


    /**
     * Iterator Interface
     *
     */
    public function getIterator() {
        return new ArrayIterator($this->data);
    }

    /**
     * Count Interface
     *
     */
    public function count()
    {
        return count($this->data);
    }
}

 

I used this for my Config library libconfig which is available on Github under the MIT license.

 Answers

23

I am not sure if this will be useful. I have noticed that the ArrayObject class is 'interesting'...

I am not sure that this is even an 'answer'. It is more an observation about this class.

It handles the 'multidimensional array' stuff correctly as standard.

You may be able to add methods to make it do more of what you wish?

<?php //

class Config extends ArrayObject
{

//    private $data = array();

    public function __construct(array $data = array())
    {
        parent::__construct($data);
    }
}

$conf = new Config(array('a' => 'foo', 'b' => 'bar', 'c' => array('sub' => 'baz')));
$conf['c']['sub'] = 'notbaz';
$conf['c']['sub2'] = 'notbaz2';

var_dump($conf, $conf['c'], $conf['c']['sub']);

unset($conf['c']['sub']);

var_dump('isset?: ', isset($conf['c']['sub']));

var_dump($conf, $conf['c'], $conf['c']['sub2']);

Output:

object(Config)[1]
  public 'a' => string 'foo' (length=3)
  public 'b' => string 'bar' (length=3)
  public 'c' => 
    array
      'sub' => string 'notbaz' (length=6)
      'sub2' => string 'notbaz2' (length=7)

array
  'sub' => string 'notbaz' (length=6)
  'sub2' => string 'notbaz2' (length=7)

string 'notbaz' (length=6)

string 'isset?: ' (length=8)

boolean false

object(Config)[1]
  public 'a' => string 'foo' (length=3)
  public 'b' => string 'bar' (length=3)
  public 'c' => 
    array
      'sub2' => string 'notbaz2' (length=7)

array
  'sub2' => string 'notbaz2' (length=7)

string 'notbaz2' (length=7)
Saturday, May 29, 2021
 
Jubair
answered 7 Months ago
42
function set_val(array &$arr, $path,$val)
{
   $loc = &$arr;
   foreach(explode('.', $path) as $step)
   {
     $loc = &$loc[$step];
   }
   return $loc = $val;
}
Wednesday, March 31, 2021
 
laurent
answered 9 Months ago
94

The problem could be resolved by changing public function offsetGet($name) to public function &offsetGet($name) (by adding return by reference), but it will cause Fatal Error ("Declaration of ArrayTest::offsetGet() must be compatible with that of ArrayAccess::offsetGet()").

PHP authors screwed up with this class some time ago and now they won't change it in sake of backwards compatibility:

We found out that this is not solvable without blowing up the interface and creating a BC or providing an additional interface to support references and thereby creating an internal nightmare - actually i don't see a way we can make that work ever. Thus we decided to enforce the original design and disallow references completley.

Edit: If you still need that functionality, I'd suggest using magic method instead (__get(), __set(), etc.), because __get() returns value by reference. This will change syntax to something like this:

$arrTest->test['bar'] = 5;

Not an ideal solution of course, but I can't think of a better one.

Update: This problem was fixed in PHP 5.3.4 and ArrayAccess now works as expected:

Starting with PHP 5.3.4, the prototype checks were relaxed and it's possible for implementations of this method to return by reference. This makes indirect modifications to the overloaded array dimensions of ArrayAccess objects possible.

Wednesday, March 31, 2021
 
michele
answered 9 Months ago
39

When you set cell values individually, you have the option of setting the datatype explicitly, but when you use the fromArray() method, you don't have this option.

However, by default, PHP uses a default value binder to identify datatypes from the values passed, and set the cell datatype accordingly. This default behaviour is defined in a class /PHPExcel/Cell/DefaultValueBinder.php.

So you can create your own value binder, as described in the PHPExcel Documentation, that would set every value as a string datatype.

Something like:

class PHPExcel_Cell_MyColumnValueBinder extends PHPExcel_Cell_DefaultValueBinder implements PHPExcel_Cell_IValueBinder
{
    protected $stringColumns = [];

    public function __construct(array $stringColumnList = []) {
        // Accept a list of columns that will always be set as strings
        $this->stringColumns = $stringColumnList;
    }

    public function bindValue(PHPExcel_Cell $cell, $value = null)
    {
        // If the cell is one of our columns to set as a string...
        if (in_array($cell->getColumn(), $this->stringColumns)) {
            // ... then we cast it to a string and explicitly set it as a string
            $cell->setValueExplicit((string) $value, PHPExcel_Cell_DataType::TYPE_STRING);
            return true;
        }
        // Otherwise, use the default behaviour
        return parent::bindValue($cell, $value);
    }
}

// Instantiate our custom binder, with a list of columns, and tell PHPExcel to use it
PHPExcel_Cell::setValueBinder(new PHPExcel_Cell_MyColumnValueBinder(['A', 'B', 'C', 'E', 'F']));

$objPHPExcel = new PHPExcel();
$objPHPExcel->getActiveSheet()->fromArray($dataArray,null,"A2");
Friday, May 28, 2021
 
Wilk
answered 7 Months ago
22

This is a side effect, and was mentioned in the proposal here: http://php.markmail.org/thread/yiujwve6zdw37tpv

The feature is speed/optimization of string offsets.

Hi,

Recently I noticed that reading of string offset is performed in two steps. At first special string_offset variant of temporary_variable is created in zend_fetch_dimension_address_read() and then the real string value is created in _get_zval_ptr_var_string_offset().

I think we can create the real string in the first place. This makes 50% speed-up on string offset reading operation and allows to eliminate some checks and conditional brunches in VM.

The patch is attached (don't forget to regenerate zend_vm_execute.h to test it). However it changes behavior in one bogus case. The following code now will emit "b" (currently it generates a fatal error - cannot use string offset as an array).

$str = "abs";
var_dump($str[1][0]);

I think it's not a problem at all. "b" makes sense because "abs"[1] -> "b" and "b"[0] -> "b".

I'm going to commit the patch in case of no objections.

Thanks. Dmitry.

Saturday, May 29, 2021
 
nomie
answered 7 Months ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :
 
Share