Symfony custom query for $this->getUser() in controller - doctrine

Is it possible to use a custom query or maybe just modify the query builder used by Symfony when doing this?
public function profileAction(Request $request)
{
/** #var User */
$user = $this->getUser(); // <=== this
return $this->getUser();
}
I would like to modify the query builder in order to add some left joins and get additional info from the database. Something like this:
public function findOneByUsernameOrEmail($username)
{
$qb = $this->createQueryBuilder('u');
return $qb
->addSelect('c', 'ai', 'pp')
->leftJoin('u.country', 'c')
->leftJoin('u.avatarImage', 'ai')
->leftJoin('u.profilePicture', 'pp')
->where($qb->expr()->orX(
'u.username = :username',
'u.email = :username'
))
->setParameter('username', $username)
->getQuery()
->getOneOrNullResult()
;
}
I would like to be able to do this on demand, not every time.

The UserProvider is responsible of fetching the User from your persistence source. The configuration is done in the security.yaml configuration file.
You can find how to use your custom query in this page of the documentation https://symfony.com/doc/current/security/user_provider.html#using-a-custom-query-to-load-the-user
Unfortunately you can't do it on demand.

Related

Laravel - can I make models dynamically and have relationships on each generated model?

I need to create Models based on the alphabet as their tables names are like 'products_a', 'products_b'....'products_z'. But I don't want to create all these models as separated files,
but want to find a way to use all these tables dynamically.
And all tables are separated based on some 'shopping-mall id' values.
I've found a solution to set tables dynamically like this solution.
So what I tried before was like below.
class Products extends Model
{
use BindsDynamically;
public $timestamps = false;
public $fillable = [ 'product_name', 'reg_time'];
public static function newProduct($mall_id)
{
$rangeArr = range('a', 'z');
$product = new Products;
foreach ($rangeArr as $ar) {
if (strtolower(substr($mall_id, 0, 1)) == $ar) {
$product->setTable('product_'.$ar);
}
}
return $product;
}
}
However, now I created a Cart model and tried to use the relationship methods with all the separated tables which I cannot.
So I realized I need the separated models with relationships not just tables.
Theoretically, my database has those tables.
products_a
products_b
....
products_z
products_cart
So, the products_cart table needs to have all the products tables idx data.
I want to use relationships like 'hasMany' or 'belongsTo', therefore I need all the separated models.
Can I make all the alphabetical models dynamically and use relationship methods?
If so, how can I do this?
OK, I understand we have a nasty DB design here. But then, we don't have a DB expert and I cannot figure what can I do about this DB design.
So, please don't judge about the DB design, rather guide the better way to replace it.
Maybe you UNION all product tables and overwrite the models newModelQuery() method to achieve that
class Products extends Model
{
use BindsDynamically;
public $timestamps = false;
public $fillable = [ 'product_name', 'reg_time'];
/**
* Get a new query builder that doesn't have any global scopes or eager loading.
*
* #return \Illuminate\Database\Eloquent\Builder|static
*/
public function newModelQuery()
{
$query = parent::newModelQuery();
$rangeArr = range('a', 'z');
foreach ($rangeArr as $ar) {
//if (strtolower(substr($mall_id, 0, 1)) == $ar) {
$query->union('product_'.$ar);
//}
}
return $query;
}
}
But that's probably just one of a couple methods you'd have to overwrite this way. And this solution currently doesn't support your $mall_id. But after all, it's due to bad database design.

Eager load for collections on the fly

I have this, user has many to many property via pivot property_users.
I am making somehow reusable classes in my webapp.
These are the models with their eager loading functions:
//User model
public function properties()
{
return $this->belongsToMany(Property::class, 'property_users', 'user_id', 'property_id');
}
//Property model
public function property_users()
{
return $this->hasMany(PropertyUser::class, 'property_id', 'id');
}
//PropertyUser model
public function user()
{
return $this->belongsTo(User::class);
}
//GetProperties class
public function handle()
{
return auth()->user()->properties()->get();
}
//somewhere in a feature
$properties = $this->run(GetProperties::class);
//this returns valid properties under the logged in user
I now need to get the chat_username in property_users that belongs to this user
I manage to make it work if I loop through the properties and then doing it on the fly.
$properties = $properties->map(function($property) {
$propertyUsers = $property->property_users()->get();
$chatUsername = null;
foreach($propertyUsers as $propertyUser) {
if($propertyUser->property_id == $property->id) {
$chatUsername = $propertyUser->chat_username;
}
}
return [
'name' => $property->name,
'id' => $property->id,
'chat_username' => $chatUsername
];
});
But I am trying to reduce query on loop to reduce hits especially when they are on multiple properties in the database.
The other way is that I can add the property_users in the eager loading under GetProperties class by updating it to:
$query = Property::query();
$query->with(['property_users']);
$query->whereHas('property_users', function($qry) {
$qry->where('user_id', Auth::user()->id);
});
$properties = $query->get();
return $properties;
But I do not want to rely on adding more eager loading to the original GetProperties class as the GetProperties will get fat and I do not really need those data (let's say adding property_invoices, property_schedules, etc but not really needing it in some area).
Rather, I want to do the eager loading on the fly but with a twist! This is how I would imagine it:
Collect all the ids from the properties, do the fetch using wherein and apply all the users to the properties in a single query. This way it will be even more beautiful.
Maybe something like this: (using the original GetProperties class)
$properties = $this->run(GetProperties::class);
//this does not work. The error is: Method Illuminate\Database\Eloquent\Collection::property_users does not exist.
$property->property_users = $properties->property_users()->get();
Would be great if someone can show me how to do it.
What about eager loading only the fields you actually need?
$query->with('property_users:id,user_id');
The model will not get fat, and you will not need to do separate queries in the loop.
It is documented in the official documentation: https://laravel.com/docs/5.8/eloquent-relationships#eager-loading , see Eager Loading Specific Columns
Edit: if you want to perform the query after the GetProperties class, you will need to collect all the ids and perform a second query. I honestly don't like this second approach, because it is far slower, less performant and I consider it less elegant then adding a single line in the GetProperties class, but it is gonna work:
$properties = $this->run(GetProperties::class);
$ids = $properties->pluck('id'); // Get all the ids as an array
$propertyUsers = propertyUsers::whereIn('property_id', $ids)->get(); // Get the propertyUsers model
foreach($properties as $property) {
$property->property_users = $propertyUsers->where('property_id', $property->id); // Not even sure you can do that, proerty_users may not been writable
}
Alright, after reading here and there.
I found this article: https://laravel-news.com/eloquent-eager-loading
The solution is really nice. Simply:
$properties = $this->run(GetProperties::class);
//lazy load
$properties->load('property_users');
$properties = $properties->map(function($property) {
$user = $property->property_users->first(function($user) {
if($user->user_id == Auth::user()->id) {
return $user;
}
})->only('chat_username');
return [
'name' => $property->name,
'id' => $property->id,
'chat_username' => $user['chat_username']
];
});
After checking query logs:
//auth query done by middleware
[2019-05-21 07:59:11] local.INFO: select * from `users` where `auth_token` = ? or `temporary_auth_token` = ? limit 1 ["token_removed_for_security_purpose","token_removed_for_security_purpose"]
//These are the queries made:
[2019-05-21 07:59:11] local.INFO: select `properties`.*, `property_users`.`user_id` as `pivot_user_id`, `property_users`.`property_id` as `pivot_property_id` from `properties` inner join `property_users` on `properties`.`id` = `property_users`.`property_id` where `property_users`.`user_id` = ? [8]
[2019-05-21 07:59:11] local.INFO: select * from `property_users` where `property_users`.`property_id` in (2, 4)
This way, I can keep my GetProperties as small as possible, then just lazy load it whereever I need it.

How to use Laravel Eloquent with complex queries

I am trying to setup Eloquent for a new API that we're working on. I am using relations in a model.
Some relations are complex and aren't really suitable for a quick chained Query Builder statement. For example, I am trying to return metrics and some of those metrics are complex. Such as counting the total clicks generated by a user (it's not just a simple COUNT(*)). Here is the code that I have now:
<?php
namespace App\Models\Eloquent;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
class Affiliate extends Model
{
protected $table = 'user';
public function profile()
{
return $this->belongsTo('App\Models\Eloquent\Profile', 'id');
}
public static function clicks()
{
$sql = "SELECT
user_id,
COUNT(*) / SUM(dummy_metric) AS total_clicks
FROM clicks
WHERE something = 'true'
AND another_thing > 100 # dummy filter for example only
GROUP BY user_id";
$rows = DB::select(DB::raw($sql));
// (PSUEDO CODE) THIS IS WHAT I'D LIKE TO DO IDEALY
return $this->belongsTo($rows, user_id);
}
Possible? I'd like to be able to write our own queries without relying on Query Builder all of the time, but I still want to be able to join the relation to Eloquent.
Assuming you have Clicks model defined, find below the eloquent version of your query.
Click::select(DB:raw("user_id, COUNT(*) / SUM(dummy_metric) AS total_clicks"))
->where(/*condition*/)->groupBy("user_id")->get();
Note:
1) Where method accepts an array of conditions, so you can add more than one condition in same method. Reference
Update
I think thius should be your clicks() method:
public function clicks(){
return $this->hasMany('App\Models\Eloquent\Click',/*relevant ids*/);
}
And, now where you want a count of clicks(in controller for example), you can use following query:
$user = User::find("user_id")->with('clicks')->get();
$clicks = $user->clicks()->count();
To make it more efficient, refer to this article on Tweaking Eloquent relations
Update 2:
You can use Accessor function to retrieve total count
Add following 2 methods in User model.(change clicksCount string to anything you need )
public function clicksCount(){
return $this->hasOne('App\Models\Eloquent\Click')
->select(DB:raw("user_id, COUNT(*) count"))
->groupBy("user_id");
}
public function getClicksCountAttribute(){
return $this->clicksCount->count();
}
Now, you can use $user->clicksCount; to get the count directly.
If you are using Laravel 5.4, you can use withCount() method to easily retrieve the count. Counting Related Models

Set custom date field when new record created in Laravel

I want to be able to save a "followup date" for a student when a new student is created. This followup date needs to be 2 weeks in the future.
I have managed to do this in the controller as follows:
public function store(StudentRequest $request)
{
$data = $request->all();
$data['followup'] = Carbon::now()->addWeeks(2);
$student = Student::create($data);
// ....
}
However, this feels like a very long-winded way of doing things. I feel as though there ought to be a way to do it automatically on the Model with less code. I thought about using a Mutator, but that wouldn't work because the 'followup' field isn't actually being set from anywhere.
You can extend the save function in your model like this:
...
public function save(array $options = []) {
if(!$this->exists){ # only before creating
$this->attributes['followup'] = Carbon::now()->addWeeks(2);
}
return parent::save($options);
}
...

How to update field when delete a row in laravel

Let I have a table named customer where customer table has a field named deleted_by.
I implement softDelete in customer model. Now I want to update deleted_by when row delete. So that I can trace who delete this row.
I do search on google about it But I don't found anything.
I use laravel 4.2.8 & Eloquent
You may update the field using something like this:
$customer = Customer::find(1); // Assume 1 is the customer id
if($customer->delete()) { // If softdeleted
DB::table('customer')->where('id', $customer->id)
->update(array('deleted_by' => 'SomeNameOrUserID'));
}
Also, you may do it in one query:
// Assumed you have passed the id to the method in $id
$ts = Carbon\Carbon::now()->toDateTimeString();
$data = array('deleted_at' => $ts, 'deleted_by' => Auth::user()->id);
DB::table('customer')->where('id', $id)->update($data);
Both is done within one query, softDelete and recorded deleted_by as well.
Something like this is the way to go:
// override soft deleting trait method on the model, base model
// or new trait - whatever suits you
protected function runSoftDelete()
{
$query = $this->newQuery()->where($this->getKeyName(), $this->getKey());
$this->{$this->getDeletedAtColumn()} = $time = $this->freshTimestamp();
$deleted_by = (Auth::id()) ?: null;
$query->update(array(
$this->getDeletedAtColumn() => $this->fromDateTime($time),
'deleted_by' => $deleted_by
));
}
Then all you need is:
$someModel->delete();
and it's done.
I would rather use a Model Event for this.
<?php
class Customer extends \Eloquent {
...
public static function boot() {
parent::boot();
// We set the deleted_by attribute before deleted event so we doesn't get an error if Customer was deleted by force (without soft delete).
static::deleting(function($model){
$model->deleted_by = Auth::user()->id;
$model->save();
});
}
...
}
Then you just delete it like you would normally do.
Customer::find(1)->delete();
I know this is an old question, but what you could do (in the customer model) is the following....
public function delete()
{
$this->deleted_by = auth()->user()->getKey();
$this->save();
return parent::delete();
}
That would still allow the soft delete while setting another value just before it deletes.

Resources