In a fresh installation of Laravel 8.20.1, I have created two models Company and User with a pivot table between to facilitate a many-to-many relationship. The pivot has a role attribute.
I can add a user to a company using:
$company->users()->attach($user);
I've added a utility method addUser so that I can first check existence of the relationship to avoid duplication:
public function addUser(User $user) {
if($this->users->contains($user)) {
// if the user already has a role, update it
Log::info("User #{$user->id} present - updating");
$this->users()->updateExistingPivot($user, ['role' => 'user'], true);
} else {
// if the user doesn't have a role, add it
Log::info("User #{$user->id} not present - adding");
$this->users()->attach($user, ['role' => 'user'], true);
}
}
The first time I run this using a refreshed database, it should see that the user is not yet related to the company and run the else part of the switch to add a new user. Running this in Tinker, it appears to do this - and the logs show 'User #1 not present - adding' - but when I check for presence using contains, it returns false:
$user = User::factory()->create();
$company = Company::factory()->create();
$company->addUser($user);
print_r($company->users->contains($user)); //false
I've tried logging the queries for this function, and I they look fine - one for checking existence of the user, a second for inserting the pivot.
Also, I can see a pivot record for user #1 and company #1, and if I then test this in Tinker, I get true:
print_r(Company::find(1)->users->contains(User::find(1))); // true
It's almost as if the database is running async, which I know isn't the case in PHP. I'm using Sqlite v3.31.0.
The issue is definitely in my addUser method, as if I replace this call in Tinker with calling attach directly, it works.
I'm really keen to use this utility method (and others), because:
I want to avoid multiple pivot records for the same company/user (without using compound indexes)
I need several more utility methods such as addAdmin, addOwner, etc
Related
I have three models user,roles,model_has_roles,when i assign any role to the user from the roles table it's creating an instance of the model in model_has_roles table.I am using removeRole() laravel method for removing roles ,i am giving some details what are the roles present inside my database like(super-market,notification,all...).it's deleteing all roles except notification,all.
$roleName = implode(' ,',$roleName); // "super-market" or "all"
foreach ($roles->pluck('name')->toArray() as $roleName) {
$user->removeRole($roleName);
}
Now what i need is irrespective of the role(any role) i want to delete that role ,some of the roles it's deleteing and remove the instance of the model from the model_has_roles and some of the roles are not deleted (for example all,notification),please help me to fix this issue
there is no built-in laravel package for managing roles and permission and removeRole() is not in laravel (I dont know why you did wrote removeRole laravel method ), there is a package called laravel-permission that has the same table design of yours. if you are using this package you can find docs in here. if you are writing your own try debugging it with dd() and tinker to find out what's wrong with some that they won't be deleted.
I got it reverse in first place, you must use explode instead, and move first line into foreach loop
foreach ($roles->pluck('name')->toArray() as $roleName) {
$roleName = explode(',',$roleName);
$user->removeRole($roleName);
}
I'm using spatie/laravel-menu and spatie/laravel-persmissions in my laravel project.
I have created a permission, assigned it to a role, and assigned the role to my user. This works fine.
Then I have generated a menu the middleware way using a macro like so:
\Menu::macro('main', function () use ($request) {
return \Menu::new()
->withoutWrapperTag()
->withoutParentTag()
->setActiveClassOnLink()
->route('preparation', 'Anstehende Termine')
->route('postprocessing', 'Nachbereitung')
->routeIfCan('administrate', 'protocols', 'Protokolle')
->addItemClass('nav-link')
->setActive($request->url());
});
In my application I have two User models with different connections:
App\User; using connection_a with database db_a and
App\DirectoryA\User; using connection_b with database db_b
In the auth config the first one is defined, and using Auth::user()->can('administrate') works fine, even in the Middleware that defines the menu.
Since I have added the menu item via routeIfCan, I'm getting an error. It tells
Base table or view not found: 1146 Table 'db_b.permissions' doesn't exist (SQL: select permissions.*, model_has_permissions.model_id as pivot_model_id, model_has_permissions.permission_id as pivot_permission_id, model_has_permissions.model_type as pivot_model_type from permissions inner join model_has_permissions on permissions.id = model_has_permissions.permission_id where model_has_permissions.model_id = 1 and model_has_permissions.model_type = App\User)
What is going wrong here? It should use the App\User model. Placing a dd() at the point the framework throws the exception shows me the correct connection...
Please help.
this mean table permissions not exist on your database maybe you forgot to run php artisan migrate after install laravel-permission?
A member of spatie helped to solve the problem:
Under the hood, routeIfCan calls app(Gate::class)->allows($ability, $ablityArguments). I assume Gate behaves slightly different than Auth::user() when it comes to multiple guards.
I don't see much room in routeIfCan to add an additional $guard or $connection argument, so I suggest you use $menu->addIf(Auth::user()->can('administrate'), ...) instead.
Let's say I've multiple users - Admin, Manager, User. User can CRUD records owned by him. Manager can CRUD records owned by him and his Users. Admin can CRUD records of all. How to achieve this in Laravel?
If you have only one role use middleware.
If you have multiple roles just like you are saying you have use gates/policies provided by Laravel out of the box.
To handle admin you will use before filter
public function before($user, $ability)
{
return ($user->is_admin) ? true : null; //return null so we continue authorising further
}
To handle manager || user you will use something along these lines (most tricky one):
public someCRUDaction(User $current, Item $item) {
return $item->created_by == $current->id || $current->users->contains('id', $item->created_by); // you need to handle logic if item belongs to manager
}
Note: $item->created_by == $current->id is self explanatory and handles if current user is owner of item
How to use policies (I would go with policies) and gates:
https://laravel.com/docs/5.4/authorization#writing-policies
Remember that you don't want to validate anything just simply can $current user do action X?
Using policies you will need to check if user can do action X at least two times (in views and in controllers/routes/customized form requests - where you handle validation).
You can create a middleware which verify the role of the current user and the role of the selected user for the action.
What would be the best way to create a relationship if it doesn’t exist already, within Eloquent, or at least a central location.
This is my dilemma. A User must have a Customer model relationship. If for whatever reason that customer record doesn’t exist (some bug that stopped it from being created) - I don’t want it to throw errors when I try to retrieve it, but I also request the customer object in multiple locations so I don’t want to test for existence in all those places.
I thought of trying the following in the User model:
public function getCustomerAttribute($value) {
// check $value and create if null
}
But that doesn’t work on relationships, $value is null.
EDIT
I already create a customer upon user creation, but I have come across a situation where it wasn't created and caused exceptions in many places, so I want to fallback.
User::created(function($user) {
$customer = Customer::create([
'user_id' => $user->id
]);
});
Is it possible for you to assume when a user is created that a customer needs to be created as well? If the rest of your system depends on this assumption I would make a model event.
use App\{User, Customer}; // assuming php7.0
UserServiceProvider extends ServiceProvider
{
/**
* Boot
*/
public function boot()
{
// on a side note, we're using "created" not "creating" because the $user->id needs to exist in order to save the relationship.
User::created(function($user) {
$customer = Customer::create([
'user_id' => $user->id
]);
});
}
}
Using CakePHP 2.2, I am building an application in which each client has it's own "realm" of data and none of the other data is visible to them. For example, a client has his set of users, courses, contractors and jobs. Groups are shared among clients, but they cannot perform actions on groups. All clients can do with groups is assign them to users. So, an administrator (using ACL) can only manage data from the same client id.
All my objects (except groups, of course) have the client_id key.
Now, I know one way to get this done and actually having it working well, but it seems a bit dirty and I'm wondering if there is a better way. Being early in the project and new to CakePHP, I'm eager to get it right.
This is how I'm doing it now :
1- A user logs in. His client_id is written to session according to the data from the user's table.
$user = $this->User->read(null, $this->Auth->user('id'));
$this->Session->write('User.client_id', $user['User']['client_id']);
2- In AppController, I have a protected function that compares that session id to a given parameter.
protected function clientCheck($client_id) {
if ($this->Session->read('User.client_id') == $client_id) {
return true;
} else {
$this->Session->setFlash(__('Invalid object or view.'));
$this->redirect(array('controller' => 'user', 'action' => 'home'));
}
}
3- Im my different index actions (each index, each relevant controller), I check the client_id using a paginate condition.
public function index() {
$this->User->recursive = 0;
$this->paginate = array(
'conditions' => array('User.client_id' => $this->Session->read('User.client_id'))
);
$this->set('users', $this->paginate());
}
4- In other actions, I check the client_id before checking the HTTP request type this way.
$user = $this->User->read(null, $id);
$this->clientCheck($user['User']['client_id']);
$this->set('user', $user);
The concept is good - it's not 'dirty', and it's pretty much exactly the same as how I've handled situations like that.
You've just got a couple of lines of redundant code. First:
$this->Auth->user('id')
That method can actually get any field for the logged in user, so you can do:
$this->Auth->user('client_id')
So your two lines:
$user = $this->User->read(null, $this->Auth->user('id'));
$this->Session->write('User.client_id', $user['User']['client_id']);
Aren't needed. You don't need to re-read the User, or write anything to the session - just grab the client_id directly from Auth any time you need it.
In fact, if you read http://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#accessing-the-logged-in-user it even says you can get it from outside the context of a controller, using the static method like:
AuthComponent::user('client_id')
Though it doesn't seem you'll be needing that.
You could also apply the client_id condition to all finds for a Model by placing something in the beforeFind function in the Model.
For example, in your User model, you could do something like this:
function beforeFind( $queryData ) {
// Automatically filter all finds by client_id of logged in user
$queryData['conditions'][$this->alias . '.client_id'] = AuthComponent::user('client_id');
return $queryData;
}
Not sure if AuthComponent::user('client_id') works in the Model, but you get the idea. This will automatically apply this condition to every find in the model.
You could also use the beforeSave in the model to automatically set that client_id for you in new records.
My answer may be database engine specific as I use PostgreSQL. In my project I used different schema for every client in mysql terms that would be separate database for every client.
In public schema (common database) I store all data that needs to be shared between all clients (objects that do not have client_id in your case), for example, variable constants, profile settings and so on.
In company specific models I define
public $useDbConfig = 'company_data';
In Controller/AppController.php beforeFilter() method I have this code to set schema according to the logged in user.
if ($this->Session->check('User.Company.id')) {
App::uses('ConnectionManager', 'Model');
$dataSource = ConnectionManager::getDataSource('company_data');
$dataSource->config['schema'] =
'company_'.$this->Session->read('User.Company.id');
}
As you see I update dataSource on the fly according to used company. This does exclude any involvement of company_id in any query as only company relevant data is stored in that schema (database). Also this adds ability to scale the project.
Downside of this approach is that it creates pain in the ass to synchronize all database structures on structure change, but it can be done using exporting data, dropping all databases, recreating them with new layout and importing data back again. Just need to be sure to export data with full inserts including column names.