I'd like to be able to write a PHP class that behaves like an array and uses normal array syntax for getting & setting.
For example (where Foo is a PHP class of my making):
$foo = new Foo();
$foo['fooKey'] = 'foo value';
echo $foo['fooKey'];
I know that PHP has the _get and _set magic methods but those don't let you use array notation to access items. Python handles it by overloading __getitem__ and __setitem__.
Is there a way to do this in PHP? If it makes a difference, I'm running PHP 5.2.
If you extend ArrayObject or implement ArrayAccess then you can do what you want.
ArrayObject
ArrayAccess
Nope, casting just results in a normal PHP array -- losing whatever functionality your ArrayObject-derived class had. Check this out:
class CaseInsensitiveArray extends ArrayObject {
public function __construct($input = array(), $flags = 0, $iterator_class = 'ArrayIterator') {
if (isset($input) && is_array($input)) {
$tmpargs = func_get_args();
$tmpargs[0] = array_change_key_case($tmpargs[0], CASE_LOWER);
return call_user_func_array(array('parent', __FUNCTION__), $tmp args);
}
return call_user_func_array(array('parent', __FUNCTION__), func_get_args());
}
public function offsetExists($index) {
if (is_string($index)) return parent::offsetExists(strtolower($index));
return parent::offsetExists($index);
}
public function offsetGet($index) {
if (is_string($index)) return parent::offsetGet(strtolower($index));
return parent::offsetGet($index);
}
public function offsetSet($index, $value) {
if (is_string($index)) return parent::offsetSet(strtolower($index, $value));
return parent::offsetSet($index, $value);
}
public function offsetUnset($index) {
if (is_string($index)) return parent::offsetUnset(strtolower($index));
return parent::offsetUnset($index);
}
}
$blah = new CaseInsensitiveArray(array(
'A'=>'hello',
'bcD'=>'goodbye',
'efg'=>'Aloha',
));
echo "is array: ".is_array($blah)."\n";
print_r($blah);
print_r(array_keys($blah));
echo $blah['a']."\n";
echo $blah['BCD']."\n";
echo $blah['eFg']."\n";
echo $blah['A']."\n";
As expected, the array_keys() call fails. In addition, is_array($blah) returns false. But if you change the constructor line to:
$blah = (array)new CaseInsensitiveArray(array(
then you just get a normal PHP array (is_array($blah) returns true, and array_keys($blah) works), but all of the functionality of the ArrayObject-derived subclass is lost (in this case, case-insensitive keys no longer work). Try running the above code both ways, and you'll see what I mean.
PHP should either provide a native array in which the keys are case-insensitive, or make ArrayObject be castable to array without losing whatever functionality the subclass implements, or just make all array functions accept ArrayObject instances.
Related
Is there a way to make a read-only property of an object in PHP? I have an object with a couple arrays in it. I want to access them as I normally would an array
echo $objObject->arrArray[0];
But I don't want to be able to write to those arrays after they're constructed. It feels like a PITA to construct a local variable:
$arrArray = $objObject->getArray1();
echo $arrArray[0];
And anyways, while it keeps the array in the object pristine, it doesn't prevent me from re-writing the local array variable.
Well, the question is where do you want to prevent writing from?
The first step is making the array protected or private to prevent writing from outside of the object scope:
protected $arrArray = array();
If from "outside" of the array, a GETTER will do you fine. Either:
public function getArray() { return $this->arrArray; }
And accessing it like
$array = $obj->getArray();
or
public function __get($name) {
return isset($this->$name) ? $this->$name : null;
}
And accessing it like:
$array = $obj->arrArray;
Notice that they don't return references. So you cannot change the original array from outside the scope of the object. You can change the array itself...
If you really need a fully immutable array, you could use a Object using ArrayAccess...
Or, you could simply extend ArrayObject and overwrite all of the writing methods:
class ImmutableArrayObject extends ArrayObject {
public function append($value) {
throw new LogicException('Attempting to write to an immutable array');
}
public function exchangeArray($input) {
throw new LogicException('Attempting to write to an immutable array');
}
public function offsetSet($index, $newval) {
throw new LogicException('Attempting to write to an immutable array');
}
public function offsetUnset($index) {
throw new LogicException('Attempting to write to an immutable array');
}
}
Then, simply make $this->arrArray an instance of the object:
public function __construct(array $input) {
$this->arrArray = new ImmutableArrayObject($input);
}
It still supports most array like usages:
count($this->arrArray);
echo $this->arrArray[0];
foreach ($this->arrArray as $key => $value) {}
But if you try to write to it, you'll get a LogicException...
Oh, but realize that if you need to write to it, all you need to do (within the object) is do:
$newArray = $this->arrArray->getArrayCopy();
//Edit array here
$this->arrArray = new ImmutableArrayObject($newArray);
If you're using PHP 5+ you can do it with __set() and __get() methods.
You have to define how they work but should do just this.
Edit an example would be like this.
class Example {
private $var;
public function __get($v) {
if (is_array($v)) {
foreach () {
// handle it here
}
} else {
return $this->$v;
}
}
}
This might not be the "best" way of doing it but it'll work depending on what you need
If defined, the magic functions __get() and __set() will be called whenever a non-existing or private property is accessed. This can be used to create "get" and "set" methods for private properties, and for instance make them read-only or manipulate the data when stored or retrieved in it.
For instance:
class Foo
{
private $bar = 0;
public $baz = 4; // Public properties will not be affected by __get() or __set()
public function __get($name)
{
if($name == 'bar')
return $this->bar;
else
return null;
}
public function __set($name, $value)
{
// ignore, since Foo::bar is read-only
}
}
$myobj = new Foo();
echo $foo->bar; // Output is "0"
$foo->bar = 5;
echo $foo->bar; // Output is still "0", since the variable is read-only
See also the manual page for overloading in PHP.
For PHP 8.1+, you can use readonly properties:
class Test
{
public readonly array $arrArray;
public function __construct()
{
$this->arrArray = [1, 2, 3];
}
}
$test = new Test();
var_dump($test->arrArray); // OK
$test->arrArray = [4, 5, 6]; // Error
in the class, do this:
private $array;
function set_array($value) {
$this->array = $value;
}
then you just set like this:
$obj->set_array($new_array);
Ive got this snippet of code below which works perfectly fine. I have been profiling it and the bit of code gets used alot of times, so I want to try figure out how to write it in a way that will perform better than the current way its written.
Is there a more efficient way to write this?
function objectToArray($d) {
if (is_object($d)) {
// Gets the properties of the given object
// with get_object_vars function
$d = get_object_vars($d);
}
if (is_array($d)) {
// Return array converted to object Using __FUNCTION__ (Magic constant) for recursive call
return array_map(__FUNCTION__, $d);
}
else {
// Return array
return $d;
}
}
You could implement a toArray() method to the class that needs to be converted:
e.g.
class foo
{
protected $property1;
protected $property2;
public function __toArray()
{
return array(
'property1' => $this->property1,
'property2' => $this->property2
);
}
}
Having access to the protected properties and having the whole conversion encapsulated in the class is in my opinion the best way.
Update
One thing to note is that the get_object_vars() function will only return the publically accessible properties - Probably not what you are after.
If the above is too manual of a task the accurate way from outside the class would be to use PHP (SPL) built in ReflectionClass:
$values = array();
$reflectionClass = new \ReflectionClass($object);
foreach($reflectionClass->getProperties() as $property) {
$values[$property->getName()] = $property->getValue($object);
}
var_dump($values);
depends what kind of object it is, many standard php objects have methods built in to convert them
for example MySQLi results can be converted like this
$resultArray = $result->fetch_array(MYSQLI_ASSOC);
if its a custom class object you might consider implementing a method in that class for that purpose as AlexP sugested
Ended up going with:
function objectToArray($d) {
$d = (object) $d;
return $d;
}
function arrayToObject($d) {
$d = (array) $d;
return $d;
}
As AlexP said you can implement a method __toArray(). Alternatively to ReflexionClass (which is complex and expensive), making use of object iteration properties, you can iterate $this as follow
class Foo
{
protected $var1;
protected $var2;
public function __toArray()
{
$result = array();
foreach ($this as $key => $value) {
$result[$key] = $value;
}
return $result;
}
}
This will also iterate object attributes not defined in the class: E.g.
$foo = new Foo;
$foo->var3 = 'asdf';
var_dump($foo->__toArray());)
See example http://3v4l.org/OnVkf
This is the fastest way I have found to convert object to array. Works with Capsule as well.
function objectToArray ($object) {
return json_decode(json_encode($object, JSON_FORCE_OBJECT), true);
}
ideone
Sample Code:
<?php
$a = new ArrayObject();
$a['b'] = array('c'=>array('d'));
print_r($a);
unset($a['b']['c']);
print_r($a);
Output
ArrayObject Object
(
[b] => Array
(
[c] => Array
(
[0] => d
)
)
)
ArrayObject Object
(
[b] => Array
(
[c] => Array
(
[0] => d
)
)
)
You notice that $a['b']['c'] is still there, even after unsetting. I would expect $a to have just the one value left (b).
In my actual app, I get the following warning:
Indirect modification of overloaded element of MyClass has no effect
Where MyClass extends ArrayObject. I have a lot of code that depends on being able to unset nested elements like this, so how can I get this to work?
One way to do it
<?php
$a = new ArrayObject();
$a['b'] = array('c' => array('d'));
$d =& $a['b'];
unset($d['c']);
print_r($a['b']);
prints:
Array
(
)
Would have to think a bit longer for an explanation as to why the syntax you've originally used doesn't remove the element.
EDIT: Explanation of behavior
What's happening is the call to unset($a['b']['c']); is translated into:
$temp = $a->offsetGet('b');
unset($temp['c']);
since $temp is a copy of $a instead of a reference to it, PHP uses copy-on-write internally and creates a second array where $temp doesn't have ['b']['c'], but $a still does.
ANOTHER EDIT: Reusable Code
So, no matter which way you slice it, seems like trying to overload function offsetGet($index) to be function &offsetGet($index) leads to trouble; so here's the shortest helper method I came up w/ could add it as a static or instance method in a subclass of ArrayObject, whatever floats your boat:
function unsetNested(ArrayObject $oArrayObject, $sIndex, $sNestedIndex)
{
if(!$oArrayObject->offSetExists($sIndex))
return;
$aValue =& $oArrayObject[$sIndex];
if(!array_key_exists($sNestedIndex, $aValue))
return;
unset($aValue[$sNestedIndex]);
}
So the original code would become
$a = new ArrayObject();
$a['b'] = array('c' => array('d'));
// instead of unset($a['b']['c']);
unsetNested($a, 'b', 'c');
print_r($a['b']);
YET ANOTHER EDIT: OO Solution
OK - So I must have been scrambling this morning b/c I found an error in my code, and when revised, we can implement a solution, based on OO.
Just so you know I tried it, extension segfaults..:
/// XXX This does not work, posted for illustration only
class BadMoxuneArrayObject extends ArrayObject
{
public function &offsetGet($index)
{
$var =& $this[$index];
return $var;
}
}
Implementing a Decorator on the other hand works like a charm:
class MoxuneArrayObject implements IteratorAggregate, ArrayAccess, Serializable, Countable
{
private $_oArrayObject; // Decorated ArrayObject instance
public function __construct($mInput=null, $iFlags=0, $sIteratorClass='')
{
if($mInput === null)
$mInput = array();
if($sIteratorClass === '')
$this->_oArrayObject = new ArrayObject($mInput, $iFlags);
else
$this->_oArrayObject = new ArrayObject($mInput, $iFlags, $sIteratorClass);
}
// -----------------------------------------
// override offsetGet to return by reference
// -----------------------------------------
public function &offsetGet($index)
{
$var =& $this->_oArrayObject[$index];
return $var;
}
// ------------------------------------------------------------
// everything else is passed through to the wrapped ArrayObject
// ------------------------------------------------------------
public function append($value)
{
return $this->_oArrayObject->append($value);
}
public function asort()
{
return $this->_oArrayObject->asort();
}
public function count()
{
return $this->_oArrayObject->count();
}
public function exchangeArray($mInput)
{
return $this->_oArrayObject->exchangeArray($mInput);
}
public function getArrayCopy()
{
return $this->_oArrayObject->getArrayCopy();
}
public function getFlags()
{
return $this->_oArrayObject->getFlags();
}
public function getIterator()
{
return $this->_oArrayObject->getIterator();
}
public function getIteratorClass()
{
return $this->_oArrayObject->getIteratorClass();
}
public function ksort()
{
return $this->_oArrayObject->ksort();
}
public function natcassesort()
{
return $this->_oArrayObject->natcassesort();
}
public function offsetExists($index)
{
return $this->_oArrayObject->offsetExists($index);
}
public function offsetSet($index, $value)
{
return $this->_oArrayObject->offsetSet($index, $value);
}
public function offsetUnset($index)
{
return $this->_oArrayObject->offsetUnset($index);
}
public function serialize()
{
return $this->_oArrayObject->serialize();
}
public function setFlags($iFlags)
{
return $this->_oArrayObject->setFlags($iFlags);
}
public function setIteratorClass($iterator_class)
{
return $this->_oArrayObject->setIteratorClass($iterator_class);
}
public function uasort($cmp_function)
{
return $this->_oArrayObject->uasort($cmp_function);
}
public function uksort($cmp_function)
{
return $this->_oArrayObject->uksort($cmp_function);
}
public function unserialize($serialized)
{
return $this->_oArrayObject->unserialize($serialized);
}
}
Now this code works as desired:
$a = new MoxuneArrayObject();
$a['b'] = array('c' => array('d'));
unset($a['b']['c']);
var_dump($a);
Still have to modify some code though..; I don't see any way round that.
It seems to me that the "overloaded" bracket operator of ArrayObject is returning a copy of the nested array, and not a reference to the original. Thus, when you call $a['b'], you are getting a copy of the internal array that ArrayObject is using to store the data. Further resolving it to $a['b']['c'] is just giving you the element "c" inside a copy, so calling unset() on it is not unsetting the element "c" in the original.
ArrayObject implements the ArrayAccess interface, which is what actually allows the bracket operator to work on an object. The documentation for ArrayAccess::offsetGet indicates that, as of PHP 5.3.4, references to the original data in ArrayObject's internal array can be acquired using the =& operator, as quickshiftin indicated in his example.
You can use unset($a->b['c']); instead of unset($a['b']['c']); in case if there won't be a huge problem to do a such replacement for all same situations within your project
I seem to have a partial solution. unset seems to work if all the nested arrays are instances of ArrayObject. In order to ensure all the nested arrays are ArrayObjects as well, we can derive instead from this class:
class ArrayWrapper extends ArrayObject {
public function __construct($input=array(), $flags=ArrayObject::STD_PROP_LIST, $iterator_class='ArrayIterator') {
foreach($input as $key=>$value) {
if(is_array($value)) {
$input[$key] = new self($value, $flags, $iterator_class);
}
}
parent::__construct($input, $flags, $iterator_class);
}
public function offsetSet($offset, $value) {
parent::offsetSet($offset, is_array($value) ? new ArrayWrapper($value) : $value);
}
}
(updated for recursiveness; untested)
And then whenever you try to add a nested array, it will automatically get converted to an ArrayWrapper instead.
Unfortunately many of the other array functions, such as array_key_exists don't work on ArrayObjects.
I can't quite understand why the output of this code is '1'.
My guess is that php is not behaving like most other OO languages that I'm used to, in that the arrays that php uses must not be objects. Changing the array that is returned by the class does not change the array within the class. How would I get the class to return an array which I can edit (and has the same address as the one within the class)?
<?php
class Test
{
public $arr;
public function __construct()
{
$this->arr = array();
}
public function addToArr($i)
{
$this->arr[] = $i;
}
public function getArr()
{
return $this->arr;
}
}
$t = new Test();
$data = 5;
$t->addToArr($data);
$tobj_arr = $t->getArr();
unset($tobj_arr[0]);
$tobj_arr_fresh = $t->getArr();
echo count($tobj_arr_fresh);
?>
EDIT: I expected the output to be 0
You have to return the array by reference. That way, php returns a reference to the array, in stead of a copy.
<?php
class Test
{
public $arr;
public function __construct()
{
$this->arr = array();
}
public function addToArr($i)
{
$this->arr[] = $i;
}
public function & getArr() //Returning by reference here
{
return $this->arr;
}
}
$t = new Test();
$data = 5;
$t->addToArr($data);
$tobj_arr = &$t->getArr(); //Reference binding here
unset($tobj_arr[0]);
$tobj_arr_fresh = $t->getArr();
echo count($tobj_arr_fresh);
?>
This returns 0.
From the returning references subpage:
Unlike parameter passing, here you have to use & in both places - to
indicate that you want to return by reference, not a copy, and to
indicate that reference binding, rather than usual assignment, should
be done
Note that although this gets the job done, question is if it is a good practice. By changing class members outside of the class itself, it can become very difficult to track the application.
Because array are passed by "copy on write" by default, getArr() should return by reference:
public function &getArr()
{
return $this->arr;
}
[snip]
$tobj_arr = &$t->getArr();
For arrays that are object, use ArrayObject. Extending ArrayObject is probably better in your case.
When you unset($tobj_arr[0]); you are passing the return value of the function call, and not the actual property of the object.
When you call the function again, you get a fresh copy of the object's property which has yet to be modified since you added 5 to it.
Since the property itself is public, try changing:
unset($tobj_arr[0]);
To: unset($t->arr[0]);
And see if that gives you the result you are looking for.
You are getting "1" because you are asking PHP how many elements are in the array by using count. Remove count and use print_r($tobj_arr_fresh)
Is there a way to tell PHP how to convert your objects to ints? Ideally it would look something like
class ExampleClass
{
...
public function __toString()
{
return $this->getName();
}
public function __toInt()
{
return $this->getId();
}
}
I realize it's not supported in this exact form, but is there an easy (not-so-hacky) workaround?
---------------------- EDIT EDIT EDIT -----------------------------
Thanks everybody! The main reason I'm looking into this is I'd like to make some classes (form generators, menu classes etc) use objects instead of arrays(uniqueId => description). This is easy enough if you decide they should work only with those objects, or only with objects that extend some kind of generic object superclass.
But I'm trying to see if there's a middle road: ideally my framework classes could accept either integer-string pairs, or objects with getId() and getDescription() methods. Because this is something that must have occurred to someone else before I'd like to use the combined knowledge of stackoverflow to find out if there's a standard / best-practice way of doing this that doesn't clash with the php standard library, common frameworks etc.
I'm afraid there is no such thing. I'm not exactly sure what the reason is you need this, but consider the following options:
Adding a toInt() method, casting in the class. You're probably aware of this already.
public function toInt()
{
return (int) $this->__toString();
}
Double casting outside the class, will result in an int.
$int = (int) (string) $class;
Make a special function outside the class:
function intify($class)
{
return (int) (string) $class;
}
$int = intify($class);
Of course the __toString() method can return a string with a number in it: return '123'. Usage outside the class might auto-cast this string to an integer.
Make your objects implement ArrayAccess and Iterator.
class myObj implements ArrayAccess, Iterator
{
}
$thing = new myObj();
$thing[$id] = $name;
Then, in the code that consumes this data, you can use the same "old style" array code that you had before:
// Here, $thing can be either an array or an instance of myObj
function doSomething($thing) {
foreach ($thing as $id => $name) {
// ....
}
}
You can use retyping:
class Num
{
private $num;
public function __construct($num)
{
$this->num = $num;
}
public function __toString()
{
return (string) $this->num;
}
}
$n1 = new Num(5);
$n2 = new Num(10);
$n3 = (int) (string) $n1 + (int) (string) $n2; // 15
__toString() exists as a magic method.
http://www.php.net/manual/en/language.oop5.magic.php#language.oop5.magic.tostring
__toInt() does not.