Use object property as array key - php

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.

Related

Converting nested objects in PHP to an array [duplicate]

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;
}
}

Create Associative array with ArrayCollection::map

TL;DR: Why does Doctrine's ArrayCollection only supports mapping an array without setting a key?
I want to create an associative array (key->value) from my Doctrine entities: i.e. customerId => CustomerName, userId => userName, etc. Creating an associative array isn't rocket science, so there are many other ways to achieve this.
However, I'm still wondering why ArrayCollection:map (or a similar method) doesn't have an option to do this. Creating an array with keys is support by it's constructor method and ArrayCollection::set(). You can even create an array like this:
$arraycollection = new ArrayCollection();
$arraycollection->add('John Doe');
$arraycollection->set('foo', 'bar');
$arraycollection->set(418, 'teapot');
But you can't set keys with ArrayCollection::map(). Why? Am I the first developer that is looking for a feature like this (not very likely) or am I missing an important principle that makes it unnecessary, impossible, undesireable or a bad practice?
I found this answer Adam Wathan's blog Customizing Keys When Mapping Collections:
This problem of wanting to customize keys during a map operation is
something I get asked about pretty regularly.
I think the reason it seems like a tricky problem is because in PHP,
we use the same data type to represent both a list and a dictionary.
He uses Laravel’s Collection library:
$emailLookup = $employees->reduce(function ($emailLookup, $employee) {
$emailLookup[$employee['email']] = $employee['name'];
return $emailLookup;
}, []);
That's exactly the solution I would like to use, but it isn't in Doctrine's ArrayCollection. A pull request to add a reduce() method has been closes because of backwards compatibility.
Thanks to this example, you can implement your own class, based on Doctrine's ArrayCollection:
use Doctrine\Common\Collections\ArrayCollection;
class ExtendedArrayCollection extends ArrayCollection
{
/**
* Reduce the collection into a single value.
*
* #param \Closure $func
* #param null $initialValue
* #return mixed
*/
public function reduce(\Closure $func, $initialValue = null)
{
return array_reduce($this->toArray(), $func, $initialValue);
}
}
This is how the ArrayCollection class is described in the source code:
An ArrayCollection is a Collection implementation that wraps a regular PHP array.
Its map() method is just a wrapper of array_map() that returns a new ArrayCollection object that wraps the PHP array returned by array_map().
Just to make everything clear, array_map() calls a function (its first argument) for each element of the array; the value returned by the function is stored in the resulting array.
For example:
$input = [ 'one' => 1, 'two' => 2, 'three' => 3, ];
$output = array_map(function ($x) { return $x * $x; }, $input);
print_r($output);
outputs:
Array
(
[one] => 1
[two] => 4
[three] => 9
)
array_map() can be invoked using one or more arrays (starting with its second argument). ArrayCollection::map() calls it using only one array (the one it wraps). When it's called with a single array, array_map() preserves its string keys (but it re-numbers the numeric keys).
Revision:
ArrayCollection::map() doesn't set keys or values. It applies a function to all the values stored in the collection and returns a new collection.
If you need to put a value at a specified key in an object of type ArrayCollection, you can use the regular PHP syntax to access array elements using square brackets.
The following code is equivalent with the code you posted in the question.
$arraycollection = new ArrayCollection();
$arraycollection[] = 'John Doe';
$arraycollection['foo'] = 'bar';
$arraycollection[418] = 'teapot';
The access using square brackets works because the ArrayCollection class implements the Collection interface that extends the ArrayAccess PHP interface.
But you can't set keys with ArrayCollection::map().
Map is not a function to convert from a collection into a map.
In general map function operates on values from the list and is independent from the data structure so the behavior of ArrayCollection:map is quite natural.

PHP Iterator Custom Sort Order (Laravel 4)

I have a "Post" object, accessed via the IOC container. Various errors tell me this object's type ends up as a "Collection", which implements several interfaces, including IteratorAggregate and ArrayAccess.
I want to display a user-defined group of posts according to a specific order, e.g.:
$desired=array("69","63","70");//these represent post id's
Just sorting an array in this manner seems complex, but I want to sort my collection. I have been researching various combinations of usort(), uksort(), Eloquent's sortBy(), array_multisort()... but the most obvious solutions result in orders like 3,2,1 or 1,2,3, not 2,1,3.
The closest I have gotten to this goal is to fetch the ones I want,
//BlogController
private function myposts($desired){
$posts=$this->post->whereIn('id',$desired)->get();
...
"convert" the Collection object to an array,
$posts=$posts->toArray();
and treat the array with a custom function: source
function sortArrayByArray($array,$orderArray) {
$ordered = array();
foreach($orderArray as $key) {
if(array_key_exists($key,$array)) {
$ordered[$key] = $array[$key];
unset($array[$key]);
}
}
return $ordered + $array;
}
$sorted=sortArrayByArray($array1,$desired);
I can then vardump the array in the correct order, but since it is now an array, I can't access the $posts object in my view. Can I convert the array back into a post object?
This whole approach feels wasteful anyway, converting to an array and back... is it? Is there a more straightforward way of sorting the contents of a "Collection"?
This is a little better, perhaps (a native php function):
array_multisort($desired,$posts,SORT_STRING);
//only works with string keys
//$desire=(1,2,3) will not work!
Again, this works for arrays, but attempting directly on the "posts" object fails...
Finally, I discovered using a Presenter: https://github.com/robclancy/presenter#array-usage which works in the view, after one of the above is completed in the controller:
#foreach($posts as $post)
<?php $post=new PostPresenter($post) ?>
{{View::make('site.post.article')->with(compact('post'))}}
#endforeach
This finally works, but it still feels like a long way to do it. Is there a better way to accomplish this task? Performance concerns or best practices with one method vs. another? Thanks in advance for anyone able to help.
You can use the Collections own sort method and pass in a callback. The callback compares every two values in your collection tells the sort method which one is "higher". The sort method then sorts them accordingly. If you want a specific value order you just create mapping and sort by that. For more info check uasort
$posts->sort(function($a, $b){
$desired=array("69" => 0,"63" => 1,"70" =>2);
if(!array_key_exists($a,$desired) || !array_key_exists($b,$desired) || $a == $b){
return 0
}
else{
return ($desired[$a] < $desired[$b]) ? -1 : 1;
}
});
Alternatively to #tim's answer, you can re-assign the sorted array to a new Collection object:
$postsArray = $this->posts->toArray();
// Do some sorting/processing, then:
$newCollection = new \Illuminate\Database\Eloquent\Collection( $postsArray );

Can't access array by index, but can iterate with foreach

How come that I can't access array by index $obj->property[0] but can iterate through the array (each has only one value) and then get the value.
foreach($obj->property as $inner_obj)
{
foreach($inner_obj->property as $inner_inner_obj)
echo $inner_inner_obj->property;
}
If I access array by index I get Undefined offset: 0
Edit: Output of var_dump
array(1) {
[0]=> object(FooClass)#141 {
// yadda yadda
}
}
if you make class by implements Countable, Iterator you can make a object that work with foreach but it is not array
$obj is not array its stdClass Object and
because of that $obj->property[0] will show you Undefined offset: 0
try to use it like this for getting 1st value of property
$obj->property->property;
Because you're looping over properties of an array in a foreach. With [0] you explicitly try to access it as an array. You can have your object extend ArrayObject and make php access your object as an array which is what you want I think.
The interfaces that gives the ability to use your object as an array:
http://www.php.net/manual/en/class.arrayaccess.php
http://www.php.net/manual/en/class.countable.php
http://www.php.net/manual/en/class.iteratoraggregate.php
The object itself which you can extend:
http://www.php.net/manual/en/class.arrayobject.php
Imo, it's never a good idea to loop over an object without array interface as it means you are using public variables. My suggestion is to always make them protected or private unless not possible otherwise (SOAP?). You can access them by getters and setters.
The $obj->property could be an array without having key 0.
The keys in your array might not be numeric. So your array might be an associative array not a regular.
If you want numeric indexing try this:
$numArray = array_values($obj->property);
var_dump($numArray);
If you want to access the first value of an array use:
$first = reset($obj->property);
It works if your variable is an array.

Reference an arrays key in its value

I know this seems like something that should be averted by design, but let's just say it is bitterly needed:
Is it possible to reference the key belonging to a value while it is being initialized?
Here is what I imagine it to be (not exactly the case in which I need it, but the key is primitive as well):
$array = array(25 => "My key is " . $this->key);
I need this because the array key is used in each value. Actually the value is another array which has a value in which the first array key is used. Like I said in the comments, I want to keep it DRY. Doing it is no problem, but I want to do it good ;)
If you are writing an array yourself you can just put key value to array value like:
$array = array(25 => "My key is 25");
If you are have an array already you can use a foreach and add all keys to it's values:
foreach($array as $key => $value) {
$array[$key] = sprintf('%s %s', $value, $key);
}
Or if you just want to have an array of keys of existing array you can use either array_flip if you want to maintain key=>value, but have keys and values flipped. Or you can use array_keys if you want just an array of keys.
To make what you want: initialize an array somewhere and do not add any keys to it's value you can implement ArrayAccess, Countable and have:
public function offsetGet($offset) {
return isset($this->container[$offset])
? $this->container[$offset] . ' ' . $offset
: null;
}
or something like this. But in this case you need to have a variable that contains this array to be an instance of your ArrayAccess implementation. And depending of usage of this class you probably will need to implement more interfaces.
No, there is no way to reference the key when defining the value. Except maybe writing a preprocessor that embeds it in the string. But that would only work for primitive values.

Categories