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.
Related
When using toJSON() on an ObjectCollection the names of the properties are always based on the PHP-names. For instance:
For the column type_name the JSON property becomes TypeName, etc.
Is there a way to make Propel use the name of the field/column instead?
If you don't mind using json_encode, try using the object's toArray() with arguments:
use Map\AuditTableMap as TableMap;
$something = new Something();
$something->setSomeColumnValue("value");
echo json_encode($something->toArray(SomethingMap::TYPE_FIELDNAME));
Output:
{"some_column_value": "value"}
In other words, use the argument <ObjectName>Map::TYPE_FIELDNAME to output an array with column names.
The docs are amazing, but they're quite confusing to navigate. I found the following comment from one of the generated models in my project. This is for version 2.0#dev, which I'm using; note that it may differ in your version. (I'd suggest looking at the docs for more formal guidance, but you can take a peek at your models too.)
/**
* Exports the object as an array.
*
* You can specify the key type of the array by passing one of the class
* type constants.
*
* #param string $keyType (optional) One of the class type constants TableMap::TYPE_PHPNAME, TableMap::TYPE_CAMELNAME,
* TableMap::TYPE_COLNAME, TableMap::TYPE_FIELDNAME, TableMap::TYPE_NUM.
* Defaults to TableMap::TYPE_PHPNAME.
* #param boolean $includeLazyLoadColumns (optional) Whether to include lazy loaded columns. Defaults to TRUE.
* #param array $alreadyDumpedObjects List of objects to skip to avoid recursion
* #param boolean $includeForeignObjects (optional) Whether to include hydrated related objects. Default to FALSE.
*
* #return array an associative array containing the field names (as keys) and field values
*/
If you only want to strictly use the toJSON call, then you'll have to do some post-processing manipulation of the string, because the only option allowed with the toJSON method is to include or not include lazy-loaded columns.
$something = new Something();
$something->setSomeColumnValue("value");
$json = $something->toJSON();
$tableMap = \Propel::getDatabaseMap()->getTableMap('Something');
$columnMaps = $tableMap->getColumns();
$phpNames = array();
$columnNames = array();
foreach ($columnMaps as $columnMap) {
$phpNames[] = '"' . $columnMap->getPhpName() . '"';
$columnNames[] = '"' . $columnMap->getColumnName() . '"';
}
$json = str_replace($phpNames, $columnNames, $json);
One caveat to this code is that if the value matches one of your column names exactly, it will be replaced. The only way to eliminate this is to json_decode the JSON object and only replace the keys, but if you don't want to use json_encode, I don't suppose you'd want to use json_decode.
#Cezille07's answer is the most correct in this case. My answer is mainly to show how TableMap/ColumnMaps can be used for post-processing, which is something I didn't know about when I started with Propel.
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
);
}
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.
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?