Symfony2 - Doctrine query - php

I have problem with Doctrine query in my Symfony2 project.
That my code:
public function getLender($lender) {
$lender = str_replace("_", " ", $lender);
$this->qb->select('ld, ld.entry, ll.name, ll.logo')
->from('PageMainBundle:LoanDescription', 'ld')
->leftJoin(
'PageMainBundle:loanLender',
'll',
\Doctrine\ORM\Query\Expr\Join::WITH,
'ld.lender = ll.lenderId'
)
->where('ll.name=?1')
->setParameter(1, $lender);
return $this->qb->getQuery()->getResult();
}
When in select section i choose columns it works very well - returns values of columns. unforunelly when I try something like that:
$this->qb->select('ld')
I don't get pure values but sometkhing strange.
How can I get values of all db columns?

This "strange" thing is most probably an LoanDescription collection of object (entity) instances. So to get value of entry field you need to call $entity->getEntry() on this entity object (assuming that you have such method defined in your entity)
OR
You can use getArrayResult instead of getResult and you should get array with valies

Related

How can I fetch associated entities when fetching results as an array (hydrate array) via doctrine?

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

Laravel-translatable - "call to a member function save() on string"

(Updated) I use the laravel-translatable package and trying to insert rows with translations. When trying to save, it gives me the error "call to a member function save() on string".
I loop an object with keys and values, like: "food": "Nourriture",
and inside the loop I do a select of the Translations table:
$translationKey = \App\Translation::select('group', 'key')->where('group',
'global')->where('key', $key)->first();
I don't do exactly as the documentaion, which would have been:
$translationKey = \App\Translation::where('key', $key)->first();
The difference is that I select the columns 'group' and 'key', and I do an extra "where" to specify that group = global. Isthere anything wrong there?
Then I try to check if there is an already existing translation. If not, I insert the translation:
if($translationKey->hasTranslation('fr')) {
continue;
}else{
//insert
$translationRow = $translationKey->translateOrNew('fr')->$key = $value;
$translationRow->save();
}
I use translateOrNew instead of translate , because otherwise I get error: "Creating default object from empty value".
It seems I can't do the ->save() method because it's a string, not a model instance which it should be. So I guess there is something wrong with this line?:
$translationKey = \App\Translation::select('group', 'key')->where('group',
'global')->where('key', $key)->first();
But what is the problem?
I had some mistakes - I needed to select the whole row instead of individual columns:
$translationKey = \App\Translation::where('group', 'global')
->where('key', 'about_us')
->first();
And there were mistakes when saving the translation. My translations_translations table has a "value" column, so this worked:
$translationKey->translateOrNew($locale)->value = $value;
$translationKey->save()

Can we add custom values to a CakePHP Table Object?

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.

Building Eloquent query with "raw" column in column list

I'm using Eloquent to build a query, passing an array of columns to the get() method to specify the column names that I want returning; but I'd also like to add one calculated column
YEAR(CURDATE())-YEAR(`dateOfBirth`) - (DAYOFYEAR(CURDATE()) < DAYOFYEAR(`dateOfBirth`)) as AGE
I know that I can specify parts of a WHERE or HAVING clause as raw, or the entire query as raw if I create it manually; but I'd rather use Eloquent's fluent interface to build the query.
Is there any way I can define this one column in the SELECT list as raw so that Eloquent doesn't wrap it in backticks?
EDIT
Alternatively, is there any way I can define the model, perhaps with a callback, of creating an age property and calculating the value in PHP when the model is populated?
Alternatively, is there any way I can define the model, perhaps with a callback, of creating an age property and calculating the value in PHP when the model is populated?
You want an accessor in your model.
public function getAgeAttribute() {
// do an age calculation on $this->dateOfBirth here
return $age;
}
Calling $model->age would then spit out the result of the calculation.
After examining the Eloquent code, and noting that (with the exception of *) all entries in the fields array that's passed to the query get() method are wrapped in backticks unless they are Query\Expression objects, the solution that I came up with was:
$joins = [
];
$columnnames = [
'id',
'roleId',
'category'
]
$calculatedFields = [
new Illuminate\Database\Query\Expression(
"YEAR(CURDATE())-YEAR(`dateOfBirth`) - (DAYOFYEAR(CURDATE()) < DAYOFYEAR(`dateOfBirth`)) as age",
),
new Illuminate\Database\Query\Expression(
"CONCAT(`forename`, ' ', `surname`) as fullname",
),
];
$modelName = 'User';
$query = (empty($joins)) ?
(new $modelName)->newQuery() :
(new $modelName)->with($this->joins);
$results = $query
->get(
array_merge(
$columnNames,
$calculatedFields
);
);
Posted here for the benefit of anybody else struggling to find any documentation explaining how to do this.

Symfony app - how to add calculated fields to Propel objects?

What is the best way of working with calculated fields of Propel objects?
Say I have an object "Customer" that has a corresponding table "customers" and each column corresponds to an attribute of my object. What I would like to do is: add a calculated attribute "Number of completed orders" to my object when using it on View A but not on Views B and C.
The calculated attribute is a COUNT() of "Order" objects linked to my "Customer" object via ID.
What I can do now is to first select all Customer objects, then iteratively count Orders for all of them, but I'd think doing it in a single query would improve performance. But I cannot properly "hydrate" my Propel object since it does not contain the definition of the calculated field(s).
How would you approach it?
There are several choices. First, is to create a view in your DB that will do the counts for you, similar to my answer here. I do this for a current Symfony project I work on where the read-only attributes for a given table are actually much, much wider than the table itself. This is my recommendation since grouping columns (max(), count(), etc) are read-only anyway.
The other options are to actually build this functionality into your model. You absolutely CAN do this hydration yourself, but it's a bit complicated. Here's the rough steps
Add the columns to your Table class as protected data members.
Write the appropriate getters and setters for these columns
Override the hydrate method and within, populate your new columns with the data from other queries. Make sure to call parent::hydrate() as the first line
However, this isn't much better than what you're talking about already. You'll still need N + 1 queries to retrieve a single record set. However, you can get creative in step #3 so that N is the number of calculated columns, not the number of rows returned.
Another option is to create a custom selection method on your TablePeer class.
Do steps 1 and 2 from above.
Write custom SQL that you will query manually via the Propel::getConnection() process.
Create the dataset manually by iterating over the result set, and handle custom hydration at this point as to not break hydration when use by the doSelect processes.
Here's an example of this approach
<?php
class TablePeer extends BaseTablePeer
{
public static function selectWithCalculatedColumns()
{
// Do our custom selection, still using propel's column data constants
$sql = "
SELECT " . implode( ', ', self::getFieldNames( BasePeer::TYPE_COLNAME ) ) . "
, count(" . JoinedTablePeer::ID . ") AS calc_col
FROM " . self::TABLE_NAME . "
LEFT JOIN " . JoinedTablePeer::TABLE_NAME . "
ON " . JoinedTablePeer::ID . " = " . self::FKEY_COLUMN
;
// Get the result set
$conn = Propel::getConnection();
$stmt = $conn->prepareStatement( $sql );
$rs = $stmt->executeQuery( array(), ResultSet::FETCHMODE_NUM );
// Create an empty rowset
$rowset = array();
// Iterate over the result set
while ( $rs->next() )
{
// Create each row individually
$row = new Table();
$startcol = $row->hydrate( $rs );
// Use our custom setter to populate the new column
$row->setCalcCol( $row->get( $startcol ) );
$rowset[] = $row;
}
return $rowset;
}
}
There may be other solutions to your problem, but they are beyond my knowledge. Best of luck!
I am doing this in a project now by overriding hydrate() and Peer::addSelectColumns() for accessing postgis fields:
// in peer
public static function locationAsEWKTColumnIndex()
{
return GeographyPeer::NUM_COLUMNS - GeographyPeer::NUM_LAZY_LOAD_COLUMNS;
}
public static function polygonAsEWKTColumnIndex()
{
return GeographyPeer::NUM_COLUMNS - GeographyPeer::NUM_LAZY_LOAD_COLUMNS + 1;
}
public static function addSelectColumns(Criteria $criteria)
{
parent::addSelectColumns($criteria);
$criteria->addAsColumn("locationAsEWKT", "AsEWKT(" . GeographyPeer::LOCATION . ")");
$criteria->addAsColumn("polygonAsEWKT", "AsEWKT(" . GeographyPeer::POLYGON . ")");
}
// in object
public function hydrate($row, $startcol = 0, $rehydrate = false)
{
$r = parent::hydrate($row, $startcol, $rehydrate);
if ($row[GeographyPeer::locationAsEWKTColumnIndex()]) // load GIS info from DB IFF the location field is populated. NOTE: These fields are either both NULL or both NOT NULL, so this IF is OK
{
$this->location_ = GeoPoint::PointFromEWKT($row[GeographyPeer::locationAsEWKTColumnIndex()]); // load gis data from extra select columns See GeographyPeer::addSelectColumns().
$this->polygon_ = GeoMultiPolygon::MultiPolygonFromEWKT($row[GeographyPeer::polygonAsEWKTColumnIndex()]); // load gis data from extra select columns See GeographyPeer::addSelectColumns().
}
return $r;
}
There's something goofy with AddAsColumn() but I can't remember at the moment, but this does work. You can read more about the AddAsColumn() issues.
Here's what I did to solve this without any additional queries:
Problem
Needed to add a custom COUNT field to a typical result set used with the Symfony Pager. However, as we know, Propel doesn't support this out the box. So the easy solution is to just do something like this in the template:
foreach ($pager->getResults() as $project):
echo $project->getName() . ' and ' . $project->getNumMembers()
endforeach;
Where getNumMembers() runs a separate COUNT query for each $project object. Of course, we know this is grossly inefficient because you can do the COUNT on the fly by adding it as a column to the original SELECT query, saving a query for each result displayed.
I had several different pages displaying this result set, all using different Criteria. So writing my own SQL query string with PDO directly would be way too much hassle as I'd have to get into the Criteria object and mess around trying to form a query string based on whatever was in it!
So, what I did in the end avoids all that, letting Propel's native code work with the Criteria and create the SQL as usual.
1 - First create the [get/set]NumMembers() equivalent accessor/mutator methods in the model object that gets returning by the doSelect(). Remember, the accessor doesn't do the COUNT query anymore, it just holds its value.
2 - Go into the peer class and override the parent doSelect() method and copy all code from it exactly as it is
3 - Remove this bit because getMixerPreSelectHook is a private method of the base peer (or copy it into your peer if you need it):
// symfony_behaviors behavior
foreach (sfMixer::getCallables(self::getMixerPreSelectHook(__FUNCTION__)) as $sf_hook)
{
call_user_func($sf_hook, 'BaseTsProjectPeer', $criteria, $con);
}
4 - Now add your custom COUNT field to the doSelect method in your peer class:
// copied into ProjectPeer - overrides BaseProjectPeer::doSelectJoinUser()
public static function doSelectJoinUser(Criteria $criteria, ...)
{
// copied from parent method, along with everything else
ProjectPeer::addSelectColumns($criteria);
$startcol = (ProjectPeer::NUM_COLUMNS - ProjectPeer::NUM_LAZY_LOAD_COLUMNS);
UserPeer::addSelectColumns($criteria);
// now add our custom COUNT column after all other columns have been added
// so as to not screw up Propel's position matching system when hydrating
// the Project and User objects.
$criteria->addSelectColumn('COUNT(' . ProjectMemberPeer::ID . ')');
// now add the GROUP BY clause to count members by project
$criteria->addGroupByColumn(self::ID);
// more parent code
...
// until we get to this bit inside the hydrating loop:
$obj1 = new $cls();
$obj1->hydrate($row);
// AND...hydrate our custom COUNT property (the last column)
$obj1->setNumMembers($row[count($row) - 1]);
// more code copied from parent
...
return $results;
}
That's it. Now you have the additional COUNT field added to your object without doing a separate query to get it as you spit out the results. The only drawback to this solution is that you've had to copy all the parent code because you need to add bits right in the middle of it. But in my situation, this seemed like a small compromise to save all those queries and not write my own SQL query string.
Add an attribute "orders_count" to a Customer, and then write something like this:
class Order {
...
public function save($conn = null) {
$customer = $this->getCustomer();
$customer->setOrdersCount($customer->getOrdersCount() + 1);
$custoner->save();
parent::save();
}
...
}
You can use not only the "save" method, but the idea stays the same. Unfortunately, Propel doesn't support any "magic" for such fields.
Propel actually builds an automatic function based on the name of the linked field. Let's say you have a schema like this:
customer:
id:
name:
...
order:
id:
customer_id: # links to customer table automagically
completed: { type: boolean, default false }
...
When you build your model, your Customer object will have a method getOrders() that will retrieve all orders associated with that customer. You can then simply use count($customer->getOrders()) to get the number of orders for that customer.
The downside is this will also fetch and hydrate those Order objects. On most RDBMS, the only performance difference between pulling the records or using COUNT() is the bandwidth used to return the results set. If that bandwidth would be significant for your application, you might want to create a method in the Customer object that builds the COUNT() query manually using Creole:
// in lib/model/Customer.php
class Customer extends BaseCustomer
{
public function CountOrders()
{
$connection = Propel::getConnection();
$query = "SELECT COUNT(*) AS count FROM %s WHERE customer_id='%s'";
$statement = $connection->prepareStatement(sprintf($query, CustomerPeer::TABLE_NAME, $this->getId());
$resultset = $statement->executeQuery();
$resultset->next();
return $resultset->getInt('count');
}
...
}

Categories