When you implement the _toString method on a class, you are able to convert the object in string
$string =(string) $object
Is there an equivalent for converting in array
$array=(array) $object
From what I have tested, with this code, the attributes of the objet are transformed in index of the array, even if this object implement ArrayAccess.
I expected that casting an object with array access, I would obtain an array thith the same values I could access with the object
public class MyObject implements ArrayAccess{
private $values;
public function __construct(array $values){
$this->values=$values;
}
public function offsetSet($name,$value){
$this->values[$name]=$value;
}
//etc...
}
$myObject=new MyObject(array('foo'=>'bar');
$asArray=(array)$myObject;
print_r($asArray);
// expect array('foo'=>'bar')
// but get array('MyObjectvalues'=>array('foo'=>'bar'));
I also Notice that the native ArrayObject class has a the behavior I expected
No, there is no magic function to cast object as array.
ArrayObject is implemented with C and has weird specific behaviors.
Implement custom method asArray and use it.
Actually, it's impossible to write a general function:
/*
* #return array ArrayAccess object converted into an array
*/
function (ArrayAccess $arrayAccessObject): array { /* ... */ }
Why? Because ArrayAccess interface just gives a way to use $aa[/*argument*/] syntax, but does not give a way to iterate over all possible arguments.
We used to think that array has a finite number of keys. However ArrayAccess let us create objects having an infinite set of keys (note, the same concerns Traversable: i.e. prime numbers are "traversable").
For example, one can write a class, implementing ArrayAccess, that acts like a HTTP client with a cache (I'm not saying that it's a good idea; it's just an example). Then offsetExists($url) tells if a URL gives 200 or not, offsetGet($url) returns a content of a URL, offsetUnset($url) clears cached content, offsetSet throws a LogicException, 'cause setting a value makes no sense in this context.
// ...
if (empty($client['https://example.com/file.csv'])) {
throw new RuntimeException('Cannot download the file');
}
$content = $client['https://example.com/file.csv'];
// ...
Or maybe one wants to read/write/unset (delete) files with ArrayAccess.
Or maybe something like (set of even numbers is infinite):
$even = new EvenNumberChecker(); // EvenNumberChecker implements ArrayAccess
$even[2]; // true
$even[3]; // false
$even[5.6]; // throws UnexpectedValueException
isset($even[7.8]); // false
$even[0] = $value; // throws LogicException
ArrayAccess objects from academic examples above cannot be converted into finite arrays.
You can use json_decode and json_encode to get the most generic function for it:
public static function toArray(ArrayAccess $array): array
{
return json_decode(
json_encode($array),
true
);
}
Related
I've built a singleton class with chaining methods (to be used in a template).
To make chaining work I need to return new static. It allows the next chain to be added. The problem I have is that I don't want to return the static object if there are no more chains.
Example
<?php
class bread {
public static $array;
public static function blueprints() {
static::$array = array('some', 'values');
return new static;
}
public static function fields() {
return static::$array;
}
}
$blueprints = bread::blueprints();
$fields = bread::blueprints()->fields();
print_r($blueprint) // Returns object - FAIL
print_r($fields ) // Returns array - OK
In the example above I want $blueprints to return an array, because there are no more methods chained on it.
How can that be done?
The simple answer is you cannot do what you want.
Method chaining is not a special thing for Php.
For your example
bread::blueprints()->fields();
This is not different than:
$tmp = bread::blueprints();
$tmp->fields();
So because of the Php does not know the context where the result will be used of it cannot change the return type.
Here is another version of this question:
Check if call is method chaining
However, your class can implement ArrayAccess interface.This will allow you to treat the object like an array without casting and you get total control over how the members are used.
You can try this:
$blueprints = (array)bread::blueprints();
I have database rows containing serialized objects.
I want to deserialize these, but the class has changed, some properties went private so the deserialization no longer works.
Is there a way to force deserialization to an array or a stdClass? (or anything that won't cause an error upon deserialization)
I want to avoid migrating data with a script. I'd rather have backward compatibility with the objects serialized in the old format.
Not really, or at least i would be pretty afraid to use something like this in production.
However, unserialize will use the autoload system or the function name specified in the unserialize_callback_func ini setting. So with a little hacking you can make this work:
// this a serialized object with the class "SomeMissingClass"
$str = 'O:16:"SomeMissingClass":1:{s:1:"a";s:1:"b";}';
ini_set('unserialize_callback_func', 'define_me'); // set your callback_function
// unserialize will pass in the desired class name
function define_me($classname) {
// just create a class that has some nice accessors to it
eval("class $classname extends ArrayObject {}");
}
$object = unserialize($str);
print $object['a']; // should print 'b'
You can use something like this to migrate your data to a little more handy format.
UPDATE:
I've consulted with my repressed memories on this (i've faced something like this once) and remembered an other solution:
So you have your SomeClass with a private property named $a
class SomeClass {
private $a;
public function getA(){
return $this->a;
}
}
And you have the serialized version of it:
$str = 'O:9:"SomeClass":1:{s:1:"a";s:1:"b";}';
When you unserialize it, and dump the result it will look like this, which is no good:
$a = unserialize($str);
var_dump($a->getA()); // prints 'null'
var_dump($a);
/*
prints:
object(SomeClass)#1 (2) {
["a":"SomeClass":private]=>
NULL
["a"]=>
string(1) "b"
}
*/
Now, when an object get's unserialized, php will call it's __wakeup magic method. The data you need is there in the object, but not under the private property but a similarly named public one. You can't reach that with the $this->a since it will look for the wrong one,
however the method get_object_vars() will return these properties and you can reassign them inside a __wakeup():
class SomeClass {
private $a;
public function getA(){
return $this->a;
}
public function __wakeup(){
foreach (get_object_vars($this) as $k => $v) {
$this->{$k} = $v;
}
}
}
$str = 'O:9:"SomeClass":1:{s:1:"a";s:1:"b";}';
$a = unserialize($str);
print $a->getA();
I think it's needless to say that you should save yourself the further headache and convert your data to some dedicated data exchange format.
Aside from manually munging the data in the database itself, which is always a risky proposition, I think your only option is to roll back the class code to an older version, extract the data, then re-store it in a more sensible way that can be more easily dealt with in future code revisions.
You do have the old classes in your SVN/GIT/CVS, right?
I've created a class that keeps some information in its attributes. It contains add() method that adds a new set of information to all of the present in this class attributes.
I'd like its objects to behave like array offsets. For example, calling:
$obj = new Class[0];
would create the object containing the first set of information.
I'd also like to use foreach() loop on that class.
The changes of attributes should be denied from outside of the class, but I should have access to them.
Is that possible?
What you need is ArrayObject it implements IteratorAggregate , Traversable , ArrayAccess , Serializable , Countable altogether
Example
echo "<pre>";
$obj = new Foo(["A","B","C"]);
foreach ( $obj as $data ) {
echo $data, PHP_EOL;
}
echo reset($obj) . end($obj), PHP_EOL; // Use array functions on object
echo count($obj), PHP_EOL; // get total element
echo $obj[1] ; // you can get element
$obj[0] = "D"; // Notice: Sorry array can not be modified
Output
A
B
C
AC
3
B
Class Used
class Foo extends ArrayObject {
public function offsetSet($offset, $value) {
trigger_error("Sorry array can not be modified");
}
}
This is how you can create multiple instance with different constructor values.
$objConfig = array(
array('id'=>1 , 'name'=>'waqar') ,
array('id'=>2 , 'name'=>'alex')
);
$objects = array();
for($i=0; $i<count($objConfig) ; $i++)
{
$objects[$i] = new ClassName($objConfig[$i]);
}
You need to implement ArrayAccess interface, examples are pretty straightforward.
Anyway I really discourage you from mixing classes and array behaviour for bad design purposes: array-wise accessing should be used just to keep syntax more concise.
Take full advantage of classes, magic methods, reflection: there's a bright and happy world out there, beyond associative arrays.
In this case, why do you not just have an array of your class instances? A very simple example:
/**
* #var MyClass[]
*/
$myClasses = array();
$myClasses[] = new myClass();
Or alternatively use one of the more specialised SPL classes, here: http://php.net/manual/en/book.spl.php, such as SplObjectStorage (I haven't had a need for this, but it looks like it might be what you need)
Finally, you could roll your own, by simply creating a class that extends ArrayAccess and enforces you class type?
It really depends on what you need, for the vast majority of cases I would rely on storing classes in an array and enforcing any business logic in my model (so that array values are always the same class). This may be less performant, but assuming you're making a web app it is highly unlikely to be an issue.
So, i would like to implement something like this:
class Collection{
private $array;
public function add($object){
array_push($this->array, $object);
}
public function array(){
return $this->array;
}
}
class JustaClass{
public $myCollection;
public function __construct(){
$this->myCollection = new Collection();
}
}
$justAnObject = new JustaClass();
$justAnObject->myCollection->add(new SomeObject());
this works just fine, but i would like to work with it like i do in .Net, ie, when i want to refer to the collection Object, i would like to do it directly, like:
foreach($justAnObject->myCollection as $collectionItem)
and not like
foreach($justAnObject->myCollection->array() as $collectionItem)
Is there any way I can do this? maybe a magic method, or implementing an Iiterator-like interface?
thanks
Actually, this is what SplObjectStorage does, so no need to code anything:
The SplObjectStorage class provides a map from objects to data or, by ignoring data, an object Set. This dual purpose can be useful in many cases involving the need to uniquely identify objects.
It implements Countable, Iterator and ArrayAccess, so you can foreach it, access it with [] and use count on it. Like the description says it's a Set, so it contains no duplicate elements.
If you want to allow for duplicate elements, you can simply use ArrayIterator or ArrayObject. You can find additional Data Structures similar to the various .NET collections in
http://php.net/manual/en/spl.datastructures.php
IMO, there is no point in writing a custom class unless you also need to customize behavior of any of the options mentioned above.
Let your Collection class implement the Iterator or IteratorAggregate interfaces. There's also an ArrayIterator class, so it's really as easy as just returning an instance of that class:
class Collection implements IteratorAggregate {
private $array;
public function add($object){
array_push($this->array, $object);
}
/* required by IteratorAggregate */
public function getIterator() {
return new ArrayIterator($this->array);
}
}
You can then use your class in the following way:
$c = new Collection();
$c->add(1);
$c->add(2);
$c->add('we can even add strings');
foreach($c as $v) {
doSomething($v);
}
Problem: I am trying to extend PHP's ArrayObject as shown below. Unfortunately I can't get it to work properly when setting multi-dimensional objects and instead an error thrown as I have the strict settings enabled in PHP. (Error: Strict standards: Creating default object from empty value)
Question: How can I modify my class to automatically create non-existing levels for me?
The code:
$config = new Config;
$config->lvl1_0 = true; // Works
$config->lvl1_1->lvl2 = true; // Throws error as "lvl1" isn't set already
class Config extends ArrayObject
{
function __construct() {
parent::__construct(array(), self::ARRAY_AS_PROPS);
}
public function offsetSet($k, $v) {
$v = is_array($v) ? new self($v) : $v;
return parent::offsetSet($k, $v);
}
}
Taking a more oop view of your issue, you can create a class that models the concept of an multi-dimensional object.
The solution im posting doesn't extends from ArrayObject to achieve the goals you mention. As you tagged your question as oop, i think it´s important to reinforce the separation the way you store an object's state from how do you access it.
Hope this will help you achieve what you need!
From what you said, an multi-dimensional object is one that:
handles multiple levels of nested information
it does so by providing reading/writing access to the information via properties
behaves nicely when undefined properties are accessed. This means that, for example, you do the following on an empty instance: $config->database->host = 'localhost' the database and host levels are initialized automatically, and host will return 'localhost' when queried.
ideally, would be initialized from an associative arrays (because you can already parse config files into them)
Proposed Solution
So, how can those features be implemented?
The second one is easy: using PHP's __get and __set methods. Those will get called whenever an read/write is beign done on an inaccessible property (one that's not defined in an object).
The trick will be then not to declare any property and handle propertie's operations through those methods and map the property name being accessed as a key to an associative array used as storage. They'll provide basically an interface for accessing information stored internally.
For the third one, we need a way to create a new nesting level when a undeclared property is read.
The key point here is realizing that the returned value for the property must be an multi-dimensional object so further levels of nesting can be created from it also: whenever we´re asked for a property whose name is not present in the internal array, we´ll associate that name with a new instance of MultiDimensionalObject and return it. The returned object will be able to handle defined or undefined properties too.
When an undeclared property is written, all we have to do is assign it's name with the value provided in the internal array.
The fourth one is easy (see it on __construct implementation). We just have to make sure that we create an MultiDimensionalObject when a property's value is an array.
Finally, the fist one: the way we handle the second and third features allows us to read and write properties (declared and undeclared) in any level of nesting.
You can do things like $config->foo->bar->baz = 'hello' on an empty instance and then query for $config->foo->bar->baz successfully.
Important
Notice that MultiDimensionalObject instead of beign itself an array is it composed with an array, letting you change the way you store the object's state as needed.
Implementation
/* Provides an easy to use interface for reading/writing associative array based information */
/* by exposing properties that represents each key of the array */
class MultiDimensionalObject {
/* Keeps the state of each property */
private $properties;
/* Creates a new MultiDimensionalObject instance initialized with $properties */
public function __construct($properties = array()) {
$this->properties = array();
$this->populate($properties);
}
/* Creates properties for this instance whose names/contents are defined by the keys/values in the $properties associative array */
private function populate($properties) {
foreach($properties as $name => $value) {
$this->create_property($name, $value);
}
}
/* Creates a new property or overrides an existing one using $name as property name and $value as its value */
private function create_property($name, $value) {
$this->properties[$name] = is_array($value) ? $this->create_complex_property($value)
: $this->create_simple_property($value);
}
/* Creates a new complex property. Complex properties are created from arrays and are represented by instances of MultiDimensionalObject */
private function create_complex_property($value = array()){
return new MultiDimensionalObject($value);
}
/* Creates a simple property. Simple properties are the ones that are not arrays: they can be strings, bools, objects, etc. */
private function create_simple_property($value) {
return $value;
}
/* Gets the value of the property named $name */
/* If $name does not exists, it is initilialized with an empty instance of MultiDimensionalObject before returning it */
/* By using this technique, we can initialize nested properties even if the path to them don't exist */
/* I.e.: $config->foo
- property doesn't exists, it is initialized to an instance of MultiDimensionalObject and returned
$config->foo->bar = "hello";
- as explained before, doesn't exists, it is initialized to an instance of MultiDimensionalObject and returned.
- when set to "hello"; bar becomes a string (it is no longer an MultiDimensionalObject instance) */
public function __get($name) {
$this->create_property_if_not_exists($name);
return $this->properties[$name];
}
private function create_property_if_not_exists($name) {
if (array_key_exists($name, $this->properties)) return;
$this->create_property($name, array());
}
public function __set($name, $value) {
$this->create_property($name, $value);
}
}
Demo
Code:
var_dump(new MultiDimensionalObject());
Result:
object(MultiDimensionalObject)[1]
private 'properties' =>
array
empty
Code:
$data = array( 'database' => array ( 'host' => 'localhost' ) );
$config = new MultiDimensionalObject($data);
var_dump($config->database);
Result:
object(MultiDimensionalObject)[2]
private 'properties' =>
array
'host' => string 'localhost' (length=9)
Code:
$config->database->credentials->username = "admin";
$config->database->credentials->password = "pass";
var_dump($config->database->credentials);
Result:
object(MultiDimensionalObject)[3]
private 'properties' =>
array
'username' => string 'admin' (length=5)
'password' => string 'pass' (length=4)
Code:
$config->database->credentials->username;
Result:
admin
Implement the offsetGet method. If you are accessing a non exist property, you can create one as you like.
As you are extend ArrayObject, you should use the array way [] to set or get.
Copied pasted your code and it works fine on my PHP test box (running PHP 5.3.6). It does mention the Strict Standards warning, but it still works as expected. Here's the output from print_r:
Config Object
(
[storage:ArrayObject:private] => Array
(
[lvl1_0] => 1
[lvl1_1] => stdClass Object
(
[lvl2] => 1
)
)
)
It is worth noting that on the PHP docs there is a comment with guidance related to what you're trying to do:
sfinktah at php dot spamtrak dot org 17-Apr-2011 07:27
If you plan to derive your own class from ArrayObject, and wish to maintain complete ArrayObject functionality (such as being able to cast to an array), it is necessary to use ArrayObject's own private property "storage".
Detailed explanation is linked above but, in addition to offsetSet which you have and offsetGet which xdazz mentions, you also must implement offsetExists and offsetUnset. This shouldn't have anything to do with your current error but it is something you should be mindful of.
Update: xdazz' second-half has the answer to your problem. If you access your Config object as an array, it works without any errors:
$config = new Config;
$config[ 'lvl1_0' ] = true;
$config[ 'lvl1_1' ][ 'lvl2' ] = true;
Can you do that or are you restricted to the Object syntax for some reason?