Laravel user model conditional eager loading (with) - php

I am working on a hybrid app build with Laravel and Vue.
I have a use case where not all users have certain relations. For example a client can have a Domain and Multiple Business Units.
Currently i have set it up like this:
<?php
namespace App\Models;
use Laravel\Sanctum\HasApiTokens;
use Spatie\MediaLibrary\HasMedia;
use Illuminate\Notifications\Notifiable;
use Lab404\Impersonate\Models\Impersonate;
use Spatie\MediaLibrary\InteractsWithMedia;
use Illuminate\Database\Eloquent\Casts\AsArrayObject;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements HasMedia
{
use Traits\BaseModelTrait;
use Traits\ActiveTrait;
use InteractsWithMedia;
use Impersonate;
use HasApiTokens;
use Notifiable;
use HasFactory;
protected $hidden = [
'password', 'remember_token',
];
protected $fillable = [
'name', 'email', 'password', 'avatar',
];
protected $casts = [
'settings' => AsArrayObject::class,
'is_admin' => 'boolean',
];
protected $with = [
'domain',
'BusinessUnits'
];
public function scopeAdmin($query)
{
return $query->where('is_admin', true);
}
public function scopeEmployee($query)
{
return $query->whereNull('domain_id');
}
public function scopeClient($query)
{
return $query->whereNotNull('domain_id');
}
public function BusinessUnits()
{
return $this->belongsToMany(BusinessUnit::class, 'users_business_units_pivot');
}
public function Domain()
{
return $this->belongsTo(Domain::class);
}
}
The "problem" with this approach is that for every request 2 queries are executed for each user. I want the relations eager loaded only if the "domain_id" is not null (scopeClient).
For normal "models" i can select per page what models should be loaded etc., but for the authenticated user this is not really possible as i know.
I think i am looking for something like this:
protected $with = [
(!$this->domain_id) ? 'domain' : null,
(!$this->domain_id) ? 'BusinessUnits' : null
];
This currently generates an error: "Constant expression contains invalid operations."
Any advice and or ideas to tackle this would be appreciated!

You can try using events:
// this code should be inside your model
public static function boot()
{
parent::boot();
self::retrieved(function($model){
if($model->domain_id !== null)
{
$model->load('domain', 'BusinessUnits');
}
});
}
and obviously, you have to remove those relations from $with

To get all the user that has domains, use whereHas()
$users = User::whereHas('Domain')->with(['Domain', 'BusinessUnits'])->get();
it will lauch 3 queries, one for the users, one for the domains and one for the business units.

Related

Expand the User Model

I'm trying to expand the User Model with another Table (profile) to get a profile-picture, position, etc.
Can I override the index() function of the User Model to do that?
Current Model-Code:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
protected $fillable = [
'name',
'email',
'password',
'user_group'
];
protected $hidden = [
'password',
'remember_token',
];
protected $casts = [
'email_verified_at' => 'datetime',
];
}
What you are trying to do is setting up a relationship between the User Model and a new Profile Model. To do this you first need to create a Model Profile and it's associated Tabble profiles
php artisan make:model Profile --migration
In database\migrations there should be a file called something like that 2022_11_28_223831_create_profiles_table.php
Now you need to add a foreign key which indicates to which User this profile belongs.
public function up()
{
Schema::create('profiles', function (Blueprint $table) {
$table->id();
// $table->string('path_to_picture')
// user id
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->timestamps();
});
}
Now in your User Model add the following function
public function profile()
{
return $this->hasOne(Profile::class);
}
And in your Profile Model
public function user()
{
return $this->belongsTo(User::class);
}
Run php artisan migrate and everything should work as expected
If you want to test if the relationship works as expected create a new TestCase with
php artisan make:test ProfileUserRelationTest
In tests\Feature\ProfileUserRelationTest.php
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Models\User;
use App\Models\Profile;
use Illuminate\Support\Facades\Hash;
class ProfileUserRelationTest extends TestCase
{
use RefreshDatabase;
public function test_the_relation_between_user_and_profile_works()
{
$user = User::create([
'name' => 'John Doe',
'email' => 'jd#example.com',
'password' => Hash::make('password'),
]);
$profile = new Profile();
$profile->user_id = $user->id;
$profile->save();
$this->assertEquals($user->id, $profile->user->id);
$this->assertEquals($user->name, $profile->user->name);
$this->assertEquals($profile->id, $user->profile->id);
}
}
Now you can run php artisan test to see if everything works.
Be carefull this will refresh your database! So don't test in production.
Output should something like this
PASS Tests\Unit\ExampleTest
✓ that true is true
PASS Tests\Feature\ExampleTest
✓ the application returns a successful response
PASS Tests\Feature\ProfileUserRelationTest
✓ the relation between user and profile works
Tests: 3 passed
Time: 0.35s
Learn more about Relationships in Laravel: https://laravel.com/docs/9.x/eloquent-relationships
Learn more about migrations: https://laravel.com/docs/9.x/migrations
Alternative
$user = User::create([
'name' => 'John Doe',
'email' => 'jd#example.com',
'password' => Hash::make('password'),
]);
$user->profile()->create(...); // replace the ... with the things you want to insert you dont need to add the user_id since it will automatically added it. It will still work like the one above.

Call to undefined method Whitecube\NovaFlexibleContent\Layouts\Layout::getMedia()

I'm a bit stuck wondering what I'm doing wrong.
I'm trying to use the spatie/laravel-medialibrary & ebess/advanced-nova-media-library together with nova-flexible-content
In nova it works wonderful but can't get the image show in my blade. If anybody has any suggestions or know what I'm doing wrong.
Thanks in advance for your time :)
I followed there guide too: https://whitecube.github.io/nova-flexible-content/#/?id=usage-with-ebessadvanced-nova-media-library
My layout looks as follows:
<?php
namespace App\Nova\Flexible\Layouts;
use Whitecube\NovaFlexibleContent\Layouts\Layout;
use Spatie\MediaLibrary\HasMedia;
use Ebess\AdvancedNovaMediaLibrary\Fields\Images;
use Whitecube\NovaFlexibleContent\Concerns\HasMediaLibrary;
class Image extends Layout implements HasMedia
{
use HasMediaLibrary;
protected $name = 'image';
protected $title = 'Image';
public function fields()
{
return [
Images::make('Image', 'images'),
];
}
}
And my model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Whitecube\NovaFlexibleContent\Concerns\HasFlexible;
class Job extends Model implements HasMedia
{
use HasFactory;
use HasFlexible;
use InteractsWithMedia;
protected $guareded = [];
protected $casts = [
'publish_date' => 'date',
];
}
Controller:
public function show(Job $job)
{
return view('jobs.show', compact('job'));
}
Blade:
#foreach ($job->flexible('content') as $block)
{{ $block->getMedia('images') }}
#endforeach
composer:
"ebess/advanced-nova-media-library": "^4.0",
"spatie/laravel-medialibrary": "^10.0.0",
"whitecube/nova-flexible-content": "^1.0"
Following answer also can be found in the related Github issue.
I just solved my problem and it was that I'm casting the wrong class.
Let me explain a little bit more:
Before
protected $casts = [
'layouts' => \Whitecube\NovaFlexibleContent\Value\FlexibleCast::class
];
After
protected $casts = [
'layouts' => \App\Casts\MyFlexibleCast::class
];
Conclusion
If you're using custom layout classes like me, you need to cast the right class.

Laravel 6: User models uses UUID instead of auto-increment. Where to update DB session logic?

OK so my User models uses webpatser/laravel-uuid. All migrations are using UUID.
So now my model looks like:
<?php
namespace App\Models;
use App\Models\Traits\Uuid;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Hash;
class User extends Authenticatable
{
use Notifiable;
use Uuid;
public $incrementing = false;
public $timestamps = true;
protected $guarded = [
'uuid',
];
protected $keyType = 'string';
protected $primaryKey = 'uuid';
protected $table = 'users';
protected $dates = [
'created_at',
'updated_at',
];
protected $hidden = [
'password',
'remember_token',
];
public function setPasswordAttribute($password): void
{
$this->attributes['password'] = Hash::make($password);
}
}
I want to use database session driver. I created session table via php artisan session:table. All migrations are done. I obviously had to rename existing user_id column. I've changed it to user_uuid. I know it's not enough as I can't find the logic responsible for populating this db table. I guess it's somewhere in the vendor (Illuminate).
Where is the logic to populate my non-default session column?
Now each open the page gives:
So I know what's the issue, what's causing it, how to change it, but I don't know where to start. Thanks for any hints.
I think you would benefit of a custom session handler because the name of the column user_id is hardcoded into the addUserInformation() method.
Extend the existing DatabaseSessionHandler.php and replace the addUserInformation() method so it looks for the correct column name:
class DatabaseUuidSessionHandler extends DatabaseSessionHandler
{
protected function addUserInformation(&$payload)
{
if ($this->container->bound(Guard::class)) {
$payload['user_uuid'] = $this->userId();
}
return $this;
}
}
Register it in one of your service providers:
class SessionServiceProvider extends ServiceProvider
{
public function boot()
{
Session::extend('databaseUuid', function ($app) {
return new DatabaseUuidSessionHandler;
});
}
}
Finally update SESSION_DRIVER in your .env to use the newly created databaseUuid driver.
Remember that this is untested code and should only be used as a guideline of how this could work.

Can't authenticate with created model on Laravel 5.1

In code with the comment "THIS IS NOT WORKING", I can't authorize.
I get user object and use Auth::login($user) but it doesn't work.
User in this moment creates in database.
public function login(Request $request)
{
if(Auth::check()) {
return redirect('home');
}
if($request->isMethod('post')) {
if($request->has('username') && $request->has('password')) {
$inputs = $request->except('_token');
if(!Auth::attempt(['username' => $inputs['username'], 'password' => $inputs['password']])) {
$user = $this->registerUser($inputs);
if($user) {
Auth::login($user);
}
}
}
return (Auth::check()) ? redirect()->route('home') : redirect()->back()->with('form_error', true);
}
return view('pages.auth.login');
}
User.php maybe here is something wrong?
namespace App\Models\system;
use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
class User extends Model implements AuthenticatableContract,
AuthorizableContract,
CanResetPasswordContract
{
use Authenticatable, Authorizable, CanResetPassword;
protected $primaryKey = 'uid';
protected $table = 'eltk.dbo.system_users';
protected $guarded = [];
protected $hidden = ['password', 'remember_token'];
}
Let's start step by step.
Why did you create user on a method where you need to handle just login logic?
What's the name of your database? Is it eltk or dbo or what? Define it in config/database.php under 'database' or in .env and use in model just table name, not name of the whole table, is not necessary.
In login method you need something like this:
public function processLogin(Request $request){
if(Auth::attempt(['email' => $request->input('email'), 'password' => $request->input('password'), 'active' => 1])){
return redirect('/');
}else{
return redirect('login')->withMessage('User with this email and/or password does not exist or your account is not active.');
}
}
And btw, when you in if-else statement login user afterward you need to redirect him on some route.

Get array of Eloquent model's relations

I'm trying to get an array of all of my model's associations. I have the following model:
class Article extends Eloquent
{
protected $guarded = array();
public static $rules = array();
public function author()
{
return $this->belongsTo('Author');
}
public function category()
{
return $this->belongsTo('Category');
}
}
From this model, I'm trying to get the following array of its relations:
array(
'author',
'category'
)
I'm looking for a way to pull this array out from the model automatically.
I've found this definition of a relationsToArray method on an Eloquent model, which appears to return an array of the model's relations. It seems to use the $this->relations attribute of the Eloquent model. However, this method returns an empty array, and the relations attribute is an empty array, despite having my relations set up correctly.
What is $this->relations used for if not to store model relations? Is there any way that I can get an array of my model's relations automatically?
It's not possible because relationships are loaded only when requested either by using with (for eager loading) or using relationship public method defined in the model, for example, if a Author model is created with following relationship
public function articles() {
return $this->hasMany('Article');
}
When you call this method like:
$author = Author::find(1);
$author->articles; // <-- this will load related article models as a collection
Also, as I said with, when you use something like this:
$article = Article::with('author')->get(1);
In this case, the first article (with id 1) will be loaded with it's related model Author and you can use
$article->author->name; // to access the name field from related/loaded author model
So, it's not possible to get the relations magically without using appropriate method for loading of relationships but once you load the relationship (related models) then you may use something like this to get the relations:
$article = Article::with(['category', 'author'])->first();
$article->getRelations(); // get all the related models
$article->getRelation('author'); // to get only related author model
To convert them to an array you may use toArray() method like:
dd($article->getRelations()->toArray()); // dump and die as array
The relationsToArray() method works on a model which is loaded with it's related models. This method converts related models to array form where toArray() method converts all the data of a model (with relationship) to array, here is the source code:
public function toArray()
{
$attributes = $this->attributesToArray();
return array_merge($attributes, $this->relationsToArray());
}
It merges model attributes and it's related model's attributes after converting to array then returns it.
use this:
class Article extends Eloquent
{
protected $guarded = array();
public static $rules = array();
public $relationships = array('Author', 'Category');
public function author() {
return $this->belongsTo('Author');
}
public function category() {
return $this->belongsTo('Category');
}
}
So outside the class you can do something like this:
public function articleWithAllRelationships()
{
$article = new Article;
$relationships = $article->relationships;
$article = $article->with($relationships)->first();
}
GruBhub, thank you very much for your comments. I have corrected the typo that you mentioned.
You are right, it is dangerous to run unknown methods, hence I added a rollback after such execution.
Many thanks also to phildawson from laracasts, https://laracasts.com/discuss/channels/eloquent/get-all-model-relationships
You can use the following trait:
<?php
namespace App\Traits;
use Illuminate\Database\Eloquent\Relations\Relation;
trait EloquentRelationshipTrait
{
/**
* Get eloquent relationships
*
* #return array
*/
public static function getRelationships()
{
$instance = new static;
// Get public methods declared without parameters and non inherited
$class = get_class($instance);
$allMethods = (new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC);
$methods = array_filter(
$allMethods,
function ($method) use ($class) {
return $method->class === $class
&& !$method->getParameters() // relationships have no parameters
&& $method->getName() !== 'getRelationships'; // prevent infinite recursion
}
);
\DB::beginTransaction();
$relations = [];
foreach ($methods as $method) {
try {
$methodName = $method->getName();
$methodReturn = $instance->$methodName();
if (!$methodReturn instanceof Relation) {
continue;
}
} catch (\Throwable $th) {
continue;
}
$type = (new \ReflectionClass($methodReturn))->getShortName();
$model = get_class($methodReturn->getRelated());
$relations[$methodName] = [$type, $model];
}
\DB::rollBack();
return $relations;
}
}
Then you can implement it in any model.
<?php
namespace App;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;
use App\Traits\EloquentRelationshipTrait;
class User extends Authenticatable
{
use Notifiable, HasApiTokens, EloquentRelationshipTrait;
Finally with (new User)->getRelationships() or User::getRelationships() you will get:
[
"notifications" => [
"MorphMany",
"Illuminate\Notifications\DatabaseNotification",
],
"readNotifications" => [
"MorphMany",
"Illuminate\Notifications\DatabaseNotification",
],
"unreadNotifications" => [
"MorphMany",
"Illuminate\Notifications\DatabaseNotification",
],
"clients" => [
"HasMany",
"Laravel\Passport\Client",
],
"tokens" => [
"HasMany",
"Laravel\Passport\Token",
],
]
I have published a package in order to get all eloquent relationships from a model. Such package contains the helper "rel" to do so.
Just run (Composer 2.x is required!):
require pablo-merener/eloquent-relationships
If you are on laravel 9, you are able to run artisan command model:show

Categories