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.
Related
This is my query:
$data = Collections::select(DB:raw("REGEXP_REPLACE(tour_id,'(,2|2,|2)','') as `new_tour_id"))->get();
I want to convert this query to update all my records in the database.
This is my database table shows:
I want this result:
Since Laravel 5.x allows attribute casting so it's possible to cast attributes to another data type for converting on runtime.
In this case, just declare a protected $casts property for example:
protected $casts = [
'tour_id' => 'array', // Will converted to (Array)
];
then store your ids like this
and finally search like this :
->whereJsonContains('tour_id', 3)->update([...]);
read more :
JSON Where Clauses
Assuming that you have a model for this table as Tour what you have to do is this:
$tours = Tour::select('tour_id')
foreach($tours as $tour) {
$tour->update([
tour_id = $whatever_id_to_update
]);
}
(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()
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.
In yii2 I have model with search:
public function search($params)
{
$query = MyModel::find();
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
$this->load($params);
if (!$this->validate()) {
return $dataProvider;
}
$query->andFilterWhere(['like', 'passengers', $this->passengers]);
return $dataProvider;
}
passengers is a TEXT field in mysql table that contains serialized array.
Q: How to tell andFilterWhere() function make unserialize and compare by any element from array? Something like this: $query->andFilterWhere(['like', unserialize('passengers')[0]['name'], $this->passengers]); - means first element of array with 'name' key
Unfortunately this is a gotchya in your data structure. Storing serialized data makes it much more difficult to query. The andFilterWhere method only produces SQL level filtering, and you really need code-level filtering as SQL has no idea what PHP Serialization is. There are two ways you could go about solving this:
Post Query Filter
Depending on how large your table is, you could return all results of the search query and then use a simple loop to deserialize and drop out values that aren't in your passenger list.
Pseudo code would be:
$result = MyModel::search(...);
$filteredResult = array_filter($result->toArray(), function ($el) {
$availablePassengers = unserialize($el['passengers']);
return count(array_intersect($availablePassengers, $this->passengers));
});
Change Your Structure
Probably the best solution would be to change your data structure so passengers are stored in a many-to-many relationship. This would allow you to perform this kind of filtering with a database query and would keep your data properly normalized.
I need to get the sum of the field "valor", from the table "orcamentos".
I'm using this and it is working, but I know that this is not the right way:
//function index() from OrcamentosController.php
$orcamentoSubprojetosTotal = $this->Orcamento->query(
"SELECT
SUM(Orcamento.valor) AS TotalOrcamentoSuprojetos
FROM
orcamentos AS Orcamento
WHERE
Orcamento.subprojeto_id IS NOT NULL;"
);
$this->set(compact('orcamentoSubprojetosTotal'));
I have found this question cakephp sum() on single field (and others sum() function in cakephp query, using virtual fields to sum values in cakephp), but in the moment I add this line to my controller:
$this->Orcamento->virtualFields['total'] = 'SUM(Orcamento.valor)';
The paginate() stops working and display only one entry, like so:
Page 1 of 1, showing 1 records out of 2 total, starting on record 1, ending on 2
This is my index() function:
public function index($tipoOrcamento = null) {
$this->Orcamento->recursive = 0;
/*
$orcamentoSubprojetosTotal = $this->Orcamento->query(
"SELECT
SUM(Orcamento.valor) AS TotalOrcamentoSuprojetos
FROM
orcamentos AS Orcamento
WHERE
Orcamento.subprojeto_id IS NOT NULL;"
);
$this->set(compact('orcamentoSubprojetosTotal'));
*/
$this->set(compact('tipoOrcamento'));
if($tipoOrcamento == 'subtitulo'){
$this->set('orcamentos', $this->Paginator->paginate('Orcamento', array('Orcamento.subtitulo_id IS NOT NULL')));
}elseif($tipoOrcamento == 'subprojeto'){
$this->set('orcamentos', $this->Paginator->paginate('Orcamento', array('Orcamento.subprojeto_id IS NOT NULL')));
}else{
$this->set('orcamentos', $this->Paginator->paginate('Orcamento'));
}
}
Can I use the query() or someone can help me with the virtual field?
Thank you.
Do not use a virtual field
A virtual field is intended for things like:
public $virtualFields = array(
'name' => 'CONCAT(User.first_name, " ", User.last_name)'
);
If a virtual field is used from something which does not belong to the/a single row - it will not do what you're expecting as evident by the secondary effects on the find call paginate generates.
Use field
Instead, use the field method and pass in the expression:
$integer = $Model->field(
'SUM(valor)',
array('NOT' => array('subprojeto_id' => null))
);
Which will execute:
SELECT SUM(valor) from x where NOT (subprojecto_id IS NULL);
This will also return a scalar value, whereas calling query as shown in the question will return a nested array.
of course when you use SUM you'll get a single record
there are two things you can do:
create the virtualField just before the find() call and unset it just after the query.
using 'fields' => arra(...) in your paginator setting and list just the fields you need to retrieve and not the virtualField when you don't want to SUM