The question boils down to finding the proper way how to getPrimaryKey when iterating over a yielded result. When using select method, the result is an object of ArrayCollection which doesn't provide the getPrimaryKey method. A simple snippet
$q = UserQuery::create();
$q->select('a', 'b'); //yields an ArrayCollection object, doesn't have getPrimaryKey method when iterated
$q->find();
However,
$q = UserQuery::create();
$q->find(); //yields an ObjectCollection object, has getPrimaryKey method when iterated
Update
I have tried to use the setFormater to force using the ObjectCollection. Ultimately, it resulted in exception being thrown.
$q = UserQuery::create()
->setFormater(ModelCriteria::FORMAT_OBJECT)
->select('a', 'b')
->find(); //Error populating object
Update 2
Providing an exact use case, since it may be unclear at first what I am looking for. I have a table with >100 columns. I am providing the functionality using behaviour to not disable (not select) some of them. Thus, I am unseting some of the columns, and basing the $q->select on the remaining ones.
if (!empty($tableQuery->tableColumnsDisable)) {
$columns = $tableQuery->getTableMap()->getColumns();
foreach ($columns as $index => $column) {
if (!empty($tableQuery->tableColumnsDisable[$column->getName()])) {
unset($columns[$index]);
continue;
}
$columns[$index] = $column->getName();
}
//TODO - returns array collection, object collection needed
$tableQuery->select($columns);
}
When using select(), Propel will skip object hydration and will just return an ArrayCollection containing an array for each result row.
To retrieve the id of each result row, you need to add the column name to the select(). You can then just retrieve the value from the row arrays by using the column name:
$users = UserQuery::create()
->select(['id', 'a', 'b'])
->orderBy('c')
->find();
foreach ($users as $user) {
$id = $user['id'];
}
The select functionality is described in the documentation and in the docblock of Propel\Runtime\ActiveQuery\ModelCriteria#select() (source).
When you are using Propel 1, the functionality is the same. Read more about it in the Propel 1 documentation or the docblock of ModelCriteria#select() (source).
Related
I am fetching data from the mysql database with doctrine:
$array = $this->em->getRepository(Documents::class)->findAll();
This is the output:
For my case I want to fetch an array directly, so I created a function:
$array = $this->em->getRepository(Documents::class)->getArray();
repository:
public function getArray()
{
return $this->getEntityManager()
->getRepository(Documents::class)
->createQueryBuilder('e')
->select('e')
->getQuery()
->getResult(\Doctrine\ORM\Query::HYDRATE_ARRAY);
}
The array is created, but some fields are missing:
How can I also fetch pages and products? And I would like my data to be shown as a date +"timestamp": "02.12.2019"
Forgot about core class that will require another setup
Just use getArrayResult() function instead of getResult(). It returns an array of all data
$query = $em->createQuery("SELECT test FROM namespaceTestBundle:Test test");
$tests = $query->getArrayResult();
Query#getResult(): Retrieves a collection of objects. The result is
either a plain collection of objects (pure) or an array where the
objects are nested in the result rows (mixed).
Query#getArrayResult(): Retrieves an array graph (a nested array) that
is largely interchangeable with the object graph generated by
Query#getResult() for read-only purposes.
I Just tested that returns all result of data as an array nested:
Second soluton in other answer will work as well but they works different ways:
Also see this answer https://stackoverflow.com/a/17499629/12232340 And repository
According to this EntityRepository class, findAll don't take multiple arguments.
You need to do a "fetch-join" by adding it to the select:
public function getArray()
{
return $this->getEntityManager()
->getRepository(Documents::class)
->createQueryBuilder('e')
->select('e', 'p')
->leftJoin('e.products', 'p')
->getQuery()
->getResult(\Doctrine\ORM\Query::HYDRATE_ARRAY);
}
More info: https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/dql-doctrine-query-language.html#joins
I have a Cake Object when querying a table:
$invoices = TableRegistry::get('invoices')->find('all', ['conditions' => ['order_number =' => $orderNumber]]);
This works fine. I then want to add other array key/values to this Object, like this one:
$invoicesTmp = array();
$invoicesTmp['customer'] = "name of customer";
But $invoicesTmp is incompatible with $invoices. (one is an array, other is an CakePHP Object)
I have tried this:
compact($invoices, $invoicesTmp);
but that didn't worked.
The find() method of a Table object returns a Cake\ORM\Query object. This object is used to build SQL queries and to execute them. It has some features to define how the results from the query should be returned.
When CakePHP fetches results from the database the records are stored as an array, and CakePHP then converts them to Entity objects. A process called "hydration" of entities. If you disable hydration the records are returned as just an array.
$query = TableRegistry::get('invoices')
->find()
->where(['order_number'=>$orderNumber])
->enableHydration(false);
foreach($query as $record) {
pr($record);
}
The above creates a query object, and you can iterate over the query records because the object itself supports iteration.
The query object implements the Cake\Collection\CollectionInterface interface, which means we can perform a bunch of collection methods on it. The most common method is the toArray().
$invoices = TableRegistry::get('invoices')
->find()
->where(['order_number'=>$orderNumber])
->enableHydration(false)
->toArray();
The $invoices variable is now a valid array object holding the all the records with each record as an array object.
You can now easily use array_merge to assign extra metadata to each record.
$invoices = array_map(function($invoice) {
return array_merge(['customer'=>'name of customer'], $invoice);
}, $invoices);
$this-set(compact('invoices'));
Updated:
Based upon the comments it appears you wish to use two different tables with different column names, but those columns represent the same data.
Field Aliases
You can rename fields in the SQL query to share a common alias.
$table = TableRegistry::get($whichTable ? 'table_a' : 'table_b');
$records = $table->find()
->select([
'id',
'invoice_id',
'name' => ? $whichTable ? 'customer_name' : 'invoice_name'
])->all();
The above selects a different column for name depending upon which table is being used. This allows you to always use $record->name in your view no matter which table.
I don't like this approach, because it makes the source code of the view file appear to reference a property of the entity that doesn't really exist. You might get confused when returning to the code later.
Field Mapping
From a MVC perspective. Only the controller knows what a view needs. So it's easier if you express this knowledge as a mapping.
$map = [
'id'=>'id',
'invoice_id'=>'invoice_id',
'name' => ? $whichTable ? 'customer_name' : 'invoice_name'
];
$table = TableRegistry::get($whichTable ? 'table_a' : 'table_b');
$records = $table->find()
->select(array_values($map))
->all();
$this->set(compact('records','map'));
Later in your view to output the columns you do it like this:
foreach($records as $record) {
echo $record->get($map['name']);
}
It becomes verbose as to what is happening, and why. You can see in the view that the controller provided a mapping between something called name and the actual field. You also know that the $map variable was injected by the controller. You now know where to go to change it.
Long time reader, first time asker. I'm experienced with things like Java/C but PHP is new to me.
I'm having an issue where an assignment doesn't assign to where I'd expect it to.
I'm getting an array from a MySQL database via Eloquent methods, in particular:
$result= TableA::where('tableA.id', '=', $id)
->with('tableB.tableC')
->get();
For reference, printing $result out looks like this:
[{"id":105, /*TableA fields*/, "tableB":null},
{"id":106, /*TableA fields*/, "tableB":null},
{"id":107, /*TableA fields*/, "tableB":{/*tableB fields*/, "tableC":
{"id":104, /*TableC fields*/}}},
{"id":108, /*TableA fields*/, "tableB":{/*tableB fields*/, "tableC":
{"id":105, /*TableC fields*/}}}]
In some cases a TableA tuple will have an associated record in tableB and hence tableC, other times there isn't an associated record in tableB. If there isn't an associated record I want to go through and make a temporary "dummy" record to pass through instead of passing through null. The code I'm using to do so is:
for ($i=0; $i < count($result); $i++)
{
if($result[$i]["tableB"] == null)
{
Log:info($result); //Print line A
$result[$i]["tableB"] = OtherController::makeDummyTableB(); //Assignment line
Log::info($result); //Print line B
Log::info($result[$i]["tableB"]); //Print line C
}
}
The problem is that the assignment line doesn't assign to the "tableB" field in the object/array returned in $result. Printing $result out at print line A and B gives the same result, with "tableB" being null for the first two records. Print line C however gives the output I'm expecting, which is the dummy record I'm creating
{"tableC":{/*TableC fields*/}}
So the assignment is doing something, but it's not assigning to the field in $result that's already there, and instead is assigning it to somewhere else (That doesn't show up when I attempt to display it)
If anyone could let me know what my current code is actually doing, and how to have it do what I'm expecting (replace "tableB":null with "tableB":{"tableC":{/*TableC fields*/}} ) it'd be much appreciated
Eloquent models have a lot of "magic" going on in the background. The fields from the table are loaded into an attributes property, and the relationships are loaded into a relations property.
The issue you're running into is that tableB is a relationship field, not a table field. Its data is stored in the relations property. The relations property, however, is not directly modifiable the way you are attempting. When you attempt to modify it using $result[$i]["tableB"], that code is actually modifying the tableB field in the attributes property.
Then there is the issue of getting the data. When you attempt to read the data directly using $result[$i]["tableB"], it will first look in the attributes property, and if it isn't found there, then it will look in the relations property.
However, when you dump the entire object using Log::info($result);, any data in the relations property overwrites the data in the attributes property. So, after a direct assignment using $result[$i]["tableB"] = 'asdf', Log::info($result) will not show the change (since relations overwrites attributes), whereas Log::info($result[$i]["tableB"]) will show the change (since it looks at attributes first).
So, analyzing your code, we have:
for ($i=0; $i < count($result); $i++) {
if($result[$i]["tableB"] == null) {
// At this point:
// - tableB relation is null
// - tableB attribute does not exist
// This is a full dump, so the relations overwrites the attributes.
// tableB will show null
Log:info($result);
// After this assignment executes:
// - tableB relation will be null
// - tableB attribute will be the dummy object
$result[$i]["tableB"] = OtherController::makeDummyTableB();
// This is a full dump, so relations (null) overwrites the attributes (dummy object).
// tableB will show null
Log::info($result); //Print line B
// This is direct access, which accesses attributes (dummy object) before relations (null).
// tableB will show dummy object
Log::info($result[$i]["tableB"]); //Print line C
}
}
If you really want to go about it this way, you should use the setRelation() method:
$result[$i]->setRelation('tableB', OtherController::makeDummyTableB());
That will specifically set tableB on the relations property, which is what you're trying to do. That should get everything working for you.
Having said that, you may be able to tackle this a different way. If you're using Laravel >= 5.3 and your tableB relationship is a HasOne (5.3+) or a BelongsTo (5.4+) relationship, you can use the withDefault() functionality on the relationship so that it will automatically generate a default model when one does not exist in the database. You can read more about this in the documentation here.
So, your relationship definition would look something like:
public function tableB()
{
return $this->hasOne('App\TableB')->withDefault();
}
Now, when no tableB record exists, the relationship will load with a new empty TableB object, instead of null.
If you need something more than just an empty TableB object, you can pass a function to the withDefault() method, and that function will be used to generate the default object.
Eloquent does not return a plain PHP array, when using the get method it will return an instance of Illuminate\Support\Collection and you cannot simply assign an object like that into a Collection. To do so, you have to convert the collection to an array first:
$result = TableA::where('tableA.id', '=', $id)
->with('tableB.tableC')
->get();
// Convert collection to array
$result = $result->toArray();
for ($i=0; $i < count($result); $i++)
{
if($result[$i]["tableB"] == null)
{
Log:info($result); //Print line A
$result[$i]["tableB"] = OtherController::makeDummyTableB(); //Assignment line
Log::info($result); //Print line B
Log::info($result[$i]["tableB"]); //Print line C
}
}
Now you should have your value correctly assigned.
Try
foreach ($result as $whatever)
{
if($whatever->tableB == null)
{
$whatever->tableB = OtherController::makeDummyTableB();
}
}
The problem is that you can't assign a tableB object to a array field of your tableA model...
Your tableB model most likely (I assume, depends how you built it) is related to your tableA model via a "tableB_id" field. So if you use your assignment, you end up with
$tableA["tableB_id"] //id of the related tableB, in your case null
$tableA["tableB"] //your newly assigned model, which has nothing to do with your relationship, because that should work on tableB_id
Just don't use weird array syntax for relations
I have an Entity Category, which is linked to itself in order to form a tree (a category can have a category as a parent and a category can have a bunch of categories as children). These are marked as private inside the Entity and not exposed to the serializer.
When I do $category->getChildren()->toArray(), I get an array of the children, but when I do $this->getDoctrine()->getRepsitory('PmbLicensing:Category')->findByParent($category)->toArray(), I get an error that toArray() is not defined. I need to use the latter because the top level categories have their parent set to null, so I cannot use the former method. How do I convert the collection of categories obtained in the latter method to an array?
Also, when trying to trouble shoot, I often would like to print out variables, but when I do something like print_r($categories);, print_r((array)$categories); or var_dump($categories); the call just runs for about two minutes and then returns null. I assume it is because of the relational mapping that goes into an infinate loop, but how do I stop this from happening?
Edit: I want to convert the object (or collection of objects) to an array, because I want to build a recursive function where the children categories of the supplied category can be retrieved up to n-depth. If the supplied category can be null, in order to retrieve from the main level of categories (with parent set to null). Here is my function:
private function getRecursiveChildren(Category $category = null, $depth, $iteration)
{
$children = $this->getDoctrine()->getRepository('PmbLicensingBundle:Category')->findByParent($category);
// \Doctrine\Common\Util\Debug::dump($children); die();
if ($depth > $iteration)
foreach ($children as $child) {
$child['children'] = $this->getRecursiveChildren($child, $depth, $iteration+1);
}
return $children;
}
On the line that has $child['children'], is says that I cannot use an object as an array.
If you need the results as an array you can return them from the database as arrays.
In your CategoryRepository class:
public function findArrayByParent($categoryId)
{
// it's a good adivce from #i.am.michiel to pass only the `id` here.
// You don't need the whole category object.
$query = $this->getEntityManager()->createQuery('...')
->setParameters(array('categoryId' => $categroyId));
return $query->getArrayResult();
}
They are never converted to objects after being retrieved from the DB so you also save time and memory.
Actually your
$children = $this->getDoctrine()->getRepository('PmbLicensingBundle:Category')
->findByParent($category);`
already returns an array so you don't have to (and can't) use ->toArray().
When you loop through your categories with
foreach ($children as $child)
$child["..."] = ...
You are treating an obect $child like an array with ["..."]. That's what your error message is about.
If you can, you should probably use doctrine and let it fill the related child and parent categories. See the Doctrine Documentation on this. Then you have automatically all your Children and can access them like $category->getChildren() (This one will return an ArrayCollection). This will save you a lot of work.
Your call simply returns no categories. Btw, I think you should pass the id of the category, not the entity.
$this->getDoctrine()
->getRepository('PmbLicensing:Category')
->findByParent($category->getId());
And why use the toArray() function? ArrayCollection already are arrays with a few additionnal methods? You should be able to use an ArrayCollection whenever you used an array.
I found a very useful Doctrine function to set an attribute on a table for getting the database IDs also as keys in the resulting Doctrine_Collection. This function is documented here: http://www.doctrine-project.org/projects/orm/1.2/docs/manual/component-overview/en#collection:key-mapping
Now the question. I cannot use the table object itself, because I need to create a dynamic query on the table (and not the magic finders as in the example).
I tried this code:
$doctrineTable = Doctrine_Core::getTable($table);
$doctrineTable->setAttribute(Doctrine_Core::ATTR_COLL_KEY, "id");
$q = $doctrineTable->createQuery("t");
foreach ($filter as $c => $v) // lopp thru coumns
if (is_array($v)) // use whereIn if value is an array
$q->andWhereIn("t." . $c, $v);
elseif (is_null($v)) // use is null for null values
$q->andWhere("t." . $c . " IS NULL");
else // use where in other cases
$q->andWhere("t." . $c . "=?", $v);
return $q->fetchAll();
Unfortunately the resulting collection is still not using an associative array, but a normal one just using keys from 0 up.
Anybody has an idea how to achieve that for a query on a single table?
Cheers,
Daniel
You are looking for the INDEXBY keyword.
The INDEXBY keyword offers a way of mapping certain columns as collection / array keys. By default Doctrine indexes multiple elements to numerically indexed arrays / collections. The mapping starts from zero. In order to override this behavior you need to use INDEXBY keyword