This question already has answers here:
Converting Multidimensional Mixed Object and Array to Array while Preserving Object Names as Keys - PHP [duplicate]
(1 answer)
How do I convert an object to an array?
(11 answers)
Closed 4 years ago.
I've recently ran into an issue where I had to convert an object that functions as a model (enforcing datatypes and such) into an array for further processing. At least the public and private properties have to show up.
Looking on stack overflow, I've found various methods to do so, however most only worked for single dimensions (no nested models), while the multidimensional versions always left the full model name in array keys.
How can I get this done while keeping the code clean?
Edit: Since someone marked this as duplicate, it's not. The 2 issues linked as duplicate (one that I even referred to myself) are not the same as they either don't work for multidimensional objects, or keep array keys including the class name prefixed to the property name. I've tested both of those solutions in my search, and neither did exactly what I described.
Inspired by the given answer over here, I've changed this up a little so we don't get the name prefixes anymore, then changed it into a trait that can be use-ed inside objects. The instance then can be called with $object->toArray();. The current object is assumed to be default, but another instance can be passed instead.
When used from inside an object, all private properties will also be returned in the array.
trait Arrayable
{
public function toArray($obj = null)
{
if (is_null($obj)) {
$obj = $this;
}
$orig_obj = $obj;
// We want to preserve the object name to the array
// So we get the object name in case it is an object before we convert to an array (which we lose the object name)
if (is_object($obj)) {
$obj = (array)$obj;
}
// If obj is now an array, we do a recursion
// If obj is not, just return the value
if (is_array($obj)) {
$new = [];
//initiate the recursion
foreach ($obj as $key => $val) {
// Remove full class name from the key
$key = str_replace(get_class($orig_obj), '', $key);
// We don't want those * infront of our keys due to protected methods
$new[$key] = self::toArray($val);
}
} else {
$new = $obj;
}
return $new;
}
}
Related
Using eloquent, I am querying two sets of data like so:
$all_perms = Permission::all(); //all permissions
$role_perms = Auth::user()->roles()->permissions; //permissions belonging to a role
$role_perms is a subset of $all_perms and what I want is to loop both arrays and come out with a new array containing all permissions already assigned to a role together with those not yet assigned to a role.
What I have done is loop through both arrays in a foreach loop and if any one array belongs to both sets, I mark it by adding a check key with corresponding value 1 to the array so that I can identify is as a permission already assigned to a role.
foreach ($role_perms as $role_perm) {
foreach ($all_perms as $key => $value ) {
if (array_diff_assoc($all_perm, $role_perm)) {
$all_perm['check'] = 1;
}
}
}
but it keeps throwing the error:
array_diff_assoc(): Argument #1 is not an array
Are they better ways of doing this? Or what can I do on this one to make it work?
Thanks for any help
That's because it's a collection, not an array. If you want to get an array, try to use toArray():
$all_perms = Permission::all()->toArray();
Also, is this a typo here:
array_diff_assoc($all_perm, $role_perm);
It should be $all_perms
Try using the wonderful contains method that is available on all your collections:
foreach ($role_perms as $role_perm) {
if($all_perms->contains($role_perm))
{
// do whatever is needed
}
}
Checkout the docs for help with the contains method.
First code and then the question:
class MyArray
{
private $arrayRef;
function __construct(&$array){
$this->arrayRef = $array;
}
function addElement($newElement){
$this->arrayRef[] = $newElement;
}
function print(){
print_r($this->arrayRef);
}
}
$array = ['first', 'second'];
$arrayObject = new MyArray($array);
$arrayObject->addElement('third');
print_r($array); // prints array containing 2 elements
echo '<br/>';
$arrayObject->print(); // prints array containing 3 elements
Class member $arrayRef, in this example doesn't work as a reference to another array provided in constructor. Argument in constructor is passed by reference, but I guess that doesn't make member $arrayRef also a reference to that array as well.
Why doesn't it work like that and how to make it work?
If you still don't get what I mean: first print_r prints array containing 2 elements, even thought it may be expected to contain 3.
When I pass third element to $arrayObject via addElement() I also want it to be added in the $array that I passed to constructor of class.
The answer is actually quite simple. Yes, you pass the array by reference via &$array but this reference gets lost when you assign/copy it to the member variable. To keep the reference, you can use the =& operator like so
$this->arrayRef =& $array;
See it work in this fiddle. You can read more about it in this question/answer (just look for reference).
Beware not to use &= (which does a bitwise operation) instead of =& (which assigns by reference).
I would like to be able to access the value of a property from a single string...
$obj->Test->FGH = "Well Done!";
I have tried
var_dump($obj->{'Test->FGH'});
And
var_dump( eval( '$obj->Test->FGH' ) );
I know, the following will work, but it has to be defined from a string
var_dump ($obj->Test->FGH);
I also know the following will work, but it doesnt access the FGH property;
var_dump ($obj->{'Test'});
So how is it possible to return the value of $obj->Test->FGH, from a string?
You need to iterate through the object structure recursively until you find the property.
Here is a recursive function that does the job.
It only works if the searched value is not an object. You will have to modify it if the property you are looking for is an object, relying on wether the $props array is empty or not.
The $props argument needs to be ordered in the same way the object properties are nested.
You could also modify it to have a string as second argument, for example Test/FGH
function search_property($obj, $props) {
$prop = array_shift($props);
// If this is an object, go one level down
if (is_object($obj->$prop)) {
return search_prop($obj->$prop, $props);
}
if (!isset($obj->$prop)) {
return false;
}
return $obj->$prop;
}
$val = search_property($obj, array('Test', 'FGH'));
I have a PHP array of objects, say with two properties a and b. So for example I can do
$arr['a1']->a = $z;
$x = $arr['a1']->b;
The array is currently using the value of each object's a property as the array key, e.g.
$arr['a1']->a == 'a1'
This is so I can quickly look up the object by that property. I now need to quickly look up by b, and so want to switch the keys from being set to property a to being set to b (both are unique).
Is there an easy way to do this? In-place or into another array are both fine.
foreach($arr as $key => $object)
{
$arr2[$object->b] = $object;
}
This will create a new array that points to the same objects.
If you want them in one array, you can do as Joost suggested in the comments ($arr[$object->b] = $object; in the loop instead). However, that will only work if there are no duplicate keys between the two sets.
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?