Just finished my "friends" system in Laravel (5.4) for a projet I'm working on. I call them "connections" in my case. Everything is working fine and I'm now trying to clean everything a little bit.
From the very beginning, following tutorials and SO answers, I put every method in my User model (which is not that bad: a connection is related to a user). But I'd like to clean and put everything related to my connections into a model named Connection.
Does the User model should handle everything related to connections, or my cleaning wish makes sense and a Connection model is legit?
How to achieve that regarding the code below? How to reference to User now I'm not inside the class anymore and $this becoming pointless?
Connection model
namespace App;
use Illuminate\Support\Facades\Auth;
use Illuminate\Foundation\Auth\User as Authenticatable;
class Connection extends Authenticatable
{
const PENDING = 0;
const ACCEPTED = 1;
const REJECTED = 2;
}
User model
namespace App;
use Illuminate\Support\Facades\Auth;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
// Get connections the user initiated by himself
public function connectionsOfOwner() {
return $this->belongsToMany(User::class, 'connections', 'user_id', 'connection_id')->withPivot('status');
}
// Get connections the user has been invited to
public function connectionsOf() {
return $this->belongsToMany(User::class, 'connections', 'connection_id', 'user_id')->withPivot('status');
}
// Accessor allowing to call $this->connections
public function getConnectionsAttribute() {
if (!array_key_exists('connections', $this->relations)) {
$this->loadConnections();
}
return $this->getRelation('connections');
}
// Check if a Laravel relation named "connection" exists
protected function loadConnections() {
if (!array_key_exists('connections', $this->relations)) {
$this->setRelation('connections', $this->mergeConnections());
}
}
// Merge "connectionsOfOwner" and "connectionsOf"
protected function mergeConnections() {
return $this->connectionsOfOwner->merge($this->connectionsOf);
}
// Get pending connections
public function getPendingConnections() {
$filtered = $this->connections->filter(function ($value, $key) {
if ($value->pivot->status == 0) {
return $value;
}
});
return $filtered;
}
// Get accepted connections
public function getAcceptedConnections() {
$filtered = $this->connections->filter(function ($value, $key) {
if ($value->pivot->status == 1) {
return $value;
}
});
return $filtered;
}
// Add a connection
public function addConnection($user) {
$this->connectionsOfOwner()->attach($user->id);
}
// Accept a connection
public function acceptConnection($user) {
$this->connectionsOf()->syncWithoutDetaching([$user->id => ['status' => Connection::ACCEPTED]]);
$this->connectionsOfOwner()->attach($user->id, ['status' => Connection::ACCEPTED]);
}
// Remove a connection
public function removeConnection($user) {
$this->connectionsOfOwner()->detach($user->id);
$this->connectionsOf()->detach($user->id);
}
}
Possibly you could skip the Connection Model and keep that functionality in a trait "HasConnections" that you use in th User model.
Jeffrey Way briefly talks about this approach in Laracon2017: https://streamacon.com/video/laracon-us-2017/day-2-jeffrey-way
Good luck!
Related
I need to save the model id that is being saved and avoid another save while it's saving it, but I don't know how to persist data between requests and access it later.
JAVASCRIPT
Works perfectly well, but I can't do the same in LARAVEL 9.
// MODELS THAT ARE BEING UPDATED.
const models = {}
function handleRequest(req) {
if (models[req.model_id]) {
return
}
models[req.model_id] = true
// UPDATE MODEL.
models[req.model_id] = false
}
LARAVEL 9
Doesn't work as expected.
<?php
use App\Http\Controllers\Controller;
// MODELS THAT ARE BEING UPDATED.
$models = [];
class MyController extends Controller
{
public function index(Request $request)
{
if ($models[$model_id]) {
return;
}
$models[$model_id] = true;
// UPDATE MODEL.
$models[$model_id] = false;
}
}
Your problem is that you are trying to compare different entities in JS, you use a Function, and in Laravel you have a Class, these are different concepts and mechanics of work
You can make a property inside a class
<?php
use App\Http\Controllers\Controller;
class MyController extends Controller
{
// MODELS THAT ARE BEING UPDATED.
public $models = [];
public function index(Request $request)
{
if ($this->models[$model_id]) {
return;
}
$this->models[$model_id] = true;
// UPDATE MODEL.
$this->models[$model_id] = false;
}
}
You might consider using sessions:
session(['model_id' => 123]); // save data to session
$model_id = session('model_id'); // get data from session
I have a polymorphic relation where class (Request) can have a relationship with either class (Leave) or class (Overtime).
Each object of class (Request) belongs to a user.
I would like to setup a relationship in the User class to directly get all of their Leave or Overtime objects.
Here is how the code looks:
Class Request with:
user_id
requestable_id
requestable_type can be App\Leave or App\Overtime
class Request extends Model
{
public function requestable() {
return $this->morphTo();
}
public function user() {
return $this->belongsTo('App\User');
}
}
Class Leave
class Leave extends Model
{
public function request() {
return $this->morphOne('App\Request', 'requestable');
}
}
Class Overtime
class Overtime extends Model
{
public function request() {
return $this->morphOne('App\Request', 'requestable');
}
}
Class User
class User extends Authenticatable
{
public function requests() {
return $this->hasMany(Request::class);
}
public function leaves() {
// Need help here
}
public function overtimes() {
// And here
}
}
What I would like to do is get all leaves and overtimes a user has, so ultimately I should be able to do this:
$userLeaves = $user->leaves;
$userOvertimes = $user->overtimes;
Seems that you need a combination of polymorphic relation (that you've already defined) and hasManyThrough.
return $this->hasManyThrough(Leave::class, Request::class);
and
return $this->hasManyThrough(Overtime::class, Request::class);
respectively. But check foreign and local keys (see more info here).
you can fetch the user leaves and overtimes through the user requests by using
$requests = $user->requests->with('requestable');
but this will fetch all user requests not depending on the type, however you can fetch them depending on the type by using the leaves and overtimes function if you want and specifying the type there
User Class
public function leaves()
{
return $this->requests->where('requestable_type', 'App\Leave');
}
public function overTimes()
{
return $this->requests->where('requestable_type', 'App\OverTime');
}
Answering my own question.
Using hasManyThrough:
public function leaves() {
return $this->hasManyThrough(
Leave::class, // the class that we want objects from
Request::class, // the class sitting between this one and the target
'user_id', // this class's foreign key in the request class
'id', // foreign key in leave class
'id', // local key in this class
'requestable_id' // key of the leave in the request class
)
// we have to limit it to only Leave class
->where('requestable_type', array_search(Leave::class, Relation::morphMap()) ?: Leave::class);
}
public function overtimes() {
return $this->hasManyThrough(
Overtime::class, // the class that we want objects from
Request::class, // the class sitting between this one and the target
'user_id', // this class's foreign key in the request class
'id', // foreign key in overtime class
'id', // local key in this class
'requestable_id' // key of the overtime in the request class
)
// we have to limit it to only overtime class
->where('requestable_type', array_search(Overtime::class, Relation::morphMap()) ?: Overtime::class);
}
I have a model named 'Poll'. Inside Poll model I defined a boot method like follows:
public static function boot()
{
parent::boot();
self::created(function($model){
// dd($model);
$speakers = $model->speakers()->get();
// dd($speakers);
// What I want to do here is: create poll options relation from speakers as follows
// $poll->poll_options()->create([
// 'option' => $speaker->name,
// ]);
}
}
I am adding the speakers relation and it is working perfect.
But inside this boot method, inside self::created if I tried to get the speakers relation, it is always empty (dd($speakers) line). Is it because of the boot method runs just after the model is saved into DB and the relations not at all saved?
I am getting newly created model in the line: dd($model) mentioned in the code.
UPDATE
I tried with events also.
My Poll Model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Backpack\CRUD\CrudTrait;
use Cookie;
use App\Events\PollCreated;
class Poll extends Model
{
........
protected $events = [
'created' => PollCreated::class,
];
.......
public function speakers()
{
return $this->belongsToMany('App\Models\Speaker','poll_speaker','poll_id','speaker_id');
}
}
app/Events/PollCreated.php:
namespace App\Events;
use App\Models\Poll;
use Illuminate\Queue\SerializesModels;
class PollCreated
{
use SerializesModels;
public $poll;
/**
* Create a new event instance.
*
* #param Poll $poll
* #return void
*/
public function __construct(Poll $poll)
{
// $this->poll = $poll;
$event = $poll->event()->first();
// dd($event);
// dd($poll->speakers()->get());
// dd($poll->load('speakers'));
}
}
Here also I am not getting speakers, in the line: dd($poll->speakers()->get());
my Speaker model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Backpack\CRUD\CrudTrait;
class Speaker extends Model
{
use CrudTrait;
……..
public function polls()
{
return $this->belongsToMany('App\Models\Poll');
}
……..
}
The problem is with timing as models must always be created before they can be set in a many-to-many relationship. So there is no possible way that in a many-to-many relationship during the created event the relationship is already set as the created events are always raised before the relationships.
Anyone looking for a solution can probably experiment with the chelout/laravel-relationship-events package as this adds relationship events to models.
To be sure, I tested this out with a simple application of users and computers.
User.php
class User extends Model
{
use HasBelongsToManyEvents;
public static function boot() {
parent::boot();
self::created(function($model){
Log::info('user::created');
});
static::belongsToManyAttaching(function ($relation, $parent, $ids) {
$ids = implode(' & ', $ids);
Log::info("Attaching {$relation} {$ids} to user.");
});
static::belongsToManyAttached(function ($relation, $parent, $ids) {
$ids = implode(' & ', $ids);
Log::info("Computers {$ids} have been attached to user.");
});
}
public function computers() {
return $this->belongsToMany(Computer::class, 'user_computers');
}
}
Computer class is the same in reverse. And for the following code:
$user = User::create();
$user->computers()->attach([
Computer::create()->id,
Computer::create()->id
]);
This was the outcome:
user::created
computer::created
computer::created
Attaching computers 69 & 70 to user.
Computers 69 & 70 have been attached to user.
I've been reading the documentation up and down now, still not sure what I'm doing wrong. In my opinion the documentation is very difficult to understand for a beginner.
Anyway, I'm trying to make something akin to the Auth::user() method, where it returns additional data about a logged in user that I will be needing for this application.
I have this helper class here:
namespace App\Helpers;
use Auth;
use Illuminate\Http\Request;
use App\Models\Grouping\User;
use App\Models\Grouping\Client;
use App\Models\Grouping\Rank;
class ClientUser {
public function __construct($request) {
$this->request = $request;
}
public function client() {
return Client::find($this->request->session()->get('client_id'));
}
public function auth() {
if (Auth::check()) {
// Get the client
$client = $this->client();
// Get the client's user
$user = $client->users()->find(Auth::user()['id']);
// Get the rank of the logged in user
$rank = Rank::find($user->pivot->rank_id);
return [
'user' => $user,
'rank' => $rank,
'client' => $client
];
}
return null;
}
}
This is responsible for doing what I described, returning additional data that I can't get through Auth::user(). Now I'm trying to register this class in the AuthServiceProvider
public function register()
{
// Register client auth
$request = $this->app->request;
$this->app->singleton(ClientUser::class, function ($app) {
return new ClientUser($request);
});
}
Now what I don't understand is how I'm supposed to make this globally accessible throughout my app like Auth::user() is.
The problem with just making "importing" it is that it needs the request object, which is why I'm passing it through the service container.
Now here's where I'm stuck. I'm not able to access app in my controller or anywhere, and I can't define a Facade because a Facade expects you to return a string of the bound service that it should "alias?"
Change your service provider like this :
$this->app->bind('client.user', function ($app) {
return new ClientUser($app->request);
});
Create another class extended from Illuminate\Support\Facades\Facade.
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
class ClientUserFacade extends Facade {
public static function getFacadeAccessor(){
return "client.user";
}
}
Add 'ClientUser => ClientUserFacade::class in alias key of app.php
Synopsis
I am building a system with at least two levels of Authentication and both have separate User models and tables in the database. A quick search on google and the only solution thus far is with a MultiAuth package that shoehorns multiple drivers on Auth.
My goal
I am attempting to remove Auth which is fairly straight-forward. But I would like CustomerAuth and AdminAuth using a separate config file as per config/customerauth.php and config\adminauth.php
Solution
I'm assuming you have a package available to work on. My vendor namespace in this example will simply be: Example - all code snippets can be found following the instructions.
I copied config/auth.php to config/customerauth.php and amended the settings accordingly.
I edited the config/app.php and replaced the Illuminate\Auth\AuthServiceProvider with Example\Auth\CustomerAuthServiceProvider.
I edited the config/app.php and replaced the Auth alias with:
'CustomerAuth' => 'Example\Support\Facades\CustomerAuth',
I then implemented the code within the package for example vendor/example/src/. I started with the ServiceProvider: Example/Auth/CustomerAuthServiceProvider.php
<?php namespace Example\Auth;
use Illuminate\Auth\AuthServiceProvider;
use Example\Auth\CustomerAuthManager;
use Example\Auth\SiteGuard;
class CustomerAuthServiceProvider extends AuthServiceProvider
{
public function register()
{
$this->app->alias('customerauth', 'Example\Auth\CustomerAuthManager');
$this->app->alias('customerauth.driver', 'Example\Auth\SiteGuard');
$this->app->alias('customerauth.driver', 'Example\Contracts\Auth\SiteGuard');
parent::register();
}
protected function registerAuthenticator()
{
$this->app->singleton('customerauth', function ($app) {
$app['customerauth.loaded'] = true;
return new CustomerAuthManager($app);
});
$this->app->singleton('customerauth.driver', function ($app) {
return $app['customerauth']->driver();
});
}
protected function registerUserResolver()
{
$this->app->bind('Illuminate\Contracts\Auth\Authenticatable', function ($app) {
return $app['customerauth']->user();
});
}
protected function registerRequestRebindHandler()
{
$this->app->rebinding('request', function ($app, $request) {
$request->setUserResolver(function() use ($app) {
return $app['customerauth']->user();
});
});
}
}
Then I implemented: Example/Auth/CustomerAuthManager.php
<?php namespace Example\Auth;
use Illuminate\Auth\AuthManager;
use Illuminate\Auth\EloquentUserProvider;
use Example\Auth\SiteGuard as Guard;
class CustomerAuthManager extends AuthManager
{
protected function callCustomCreator($driver)
{
$custom = parent::callCustomCreator($driver);
if ($custom instanceof Guard) return $custom;
return new Guard($custom, $this->app['session.store']);
}
public function createDatabaseDriver()
{
$provider = $this->createDatabaseProvider();
return new Guard($provider, $this->app['session.store']);
}
protected function createDatabaseProvider()
{
$connection = $this->app['db']->connection();
$table = $this->app['config']['customerauth.table'];
return new DatabaseUserProvider($connection, $this->app['hash'], $table);
}
public function createEloquentDriver()
{
$provider = $this->createEloquentProvider();
return new Guard($provider, $this->app['session.store']);
}
protected function createEloquentProvider()
{
$model = $this->app['config']['customerauth.model'];
return new EloquentUserProvider($this->app['hash'], $model);
}
public function getDefaultDriver()
{
return $this->app['config']['customerauth.driver'];
}
public function setDefaultDriver($name)
{
$this->app['config']['customerauth.driver'] = $name;
}
}
I then implemented Example/Auth/SiteGuard.php (note the methods implemented have an additional site_ defined, this should be different for other Auth drivers):
<?php namespace Example\Auth;
use Illuminate\Auth\Guard;
class SiteGuard extends Guard
{
public function getName()
{
return 'login_site_'.md5(get_class($this));
}
public function getRecallerName()
{
return 'remember_site_'.md5(get_class($this));
}
}
I then implemented Example/Contracts/Auth/SiteGuard.php
use Illuminate\Contracts\Auth\Guard;
interface SiteGuard extends Guard {}
Finally I implemented the Facade; Example/Support/Facades/Auth/CustomerAuth.php
<?php namespace Example\Support\Facades;
class CustomerAuth extends Facade
{
protected static function getFacadeAccessor()
{
return 'customerauth';
}
}
A quick update, when trying to use these custom auth drivers with phpunit you may get the following error:
Driver [CustomerAuth] not supported.
You also need to implement this, the easiest solution is override the be method and also creating a trait similar to it:
<?php namespace Example\Vendor\Testing;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
trait ApplicationTrait
{
public function be(UserContract $user, $driver = null)
{
$this->app['customerauth']->driver($driver)->setUser($user);
}
}