ella-stinnes liked a comment+100 XP
1mo ago
You don’t need a ProjectUser model just to authorize “assigning a project to a user”.
In Laravel, the “pivot” is usually not treated as a first-class resource for authorization; instead you authorize the action on one of the real models (most commonly User or Project) and pass the other model(s) as arguments.
- Define a custom ability on UserPolicy
// app/Policies/UserPolicy.php
public function assignProject(User $actor, User $target, Project $project): bool
{
// 1) Full Admin: can link users to projects within the user's company
if ($actor->isFullAdmin()) {
return $target->company_id === $project->company_id;
}
// 2) Company Admin: only within their own company
if ($actor->isCompanyAdmin()) {
return $actor->company_id === $target->company_id
&& $actor->company_id === $project->company_id;
}
return false;
}
- Use it in controller (best place, because you have all objects)
public function update(User $user, Request $request)
{
$project = Project::findOrFail($request->project_id);
$this->authorize('assignProject', [$user, $project]);
$user->projects()->syncWithoutDetaching([$project->id]);
}
Route middleware: possible, but you’ll still need the objects
Route ->can() works well when your policy method can be resolved from route params. Since the ability needs User + Project, your route must provide both:
Route::post('/users/{user}/projects/{project}', [UserProjectController::class, 'store'])
->can('assignProject', ['user', 'project']);
This calls UserPolicy@assignProject(auth()->user(), $user, $project).
If your “edit” page doesn’t include {project} in the URL, then ->can() can only authorize a broader ability like “canManageUserProjects” (no specific project yet).
ella-stinnes wrote a reply+100 XP
1mo ago
ella-stinnes wrote a reply+100 XP
1mo ago
ella-stinnes started a new conversation+100 XP
1mo ago
ella-stinnes wrote a reply+100 XP
2mos ago
To check my understanding, I would define a gate for the page access as follows:
Gate::define('companies-index', function (User $user) {
return $user->isRole([UserRole::Administrator, UserRole::CompanyAdministrator]);
});
Then the policy would determine whether they can viewAny and/or view a specific model as follows?
A company administrator would not be able to viewany so a query scope would be applied to the index page query?
public function viewAny(User $user): bool
{
return $user->isRole([UserRole::Administrator]);
}
public function view(User $user, Company $company): bool
{
if ($user->isRole([UserRole::Administrator]))
return true;
if($user->isRole([UserRole::CompanyAdministrator])) {
return $user->company_id === $company->id;
return false;
}
ella-stinnes started a new conversation+100 XP
2mos ago
Both an administrator and company administrator should be able to access the companies.index page.
-
Administrator - should be able to list all companies.
-
Company Administrator - should only list the companies they are related to.
I've seen differing opinions on the policy viewAny method.
Should I be using this to define whether the user role can access the index page as I have below:
public function viewAny(User $user): bool
{
return $user->isRole([UserRole::Administrator, UserRole::CompanyAdministrator]);
}
viewAny implies the CompanyAdministrator role can view any company.
Should I therefore be creating another method for allowing access to the index page (if so, what would you name this)?
ella-stinnes liked a comment+100 XP
2mos ago
@ella-stinnes Sounds like data providers and/or the “test with” utility would be a good solution for your use cases. They basically let you re-run a single test case multiple times, but with a different input each time.
The application has 8 different user roles, some users can access the full CRUD, some are read only and others cannot see the company details at all.
Should I be creating 8 tests for the different user roles?
Using a data provider, you’d have a single test case (e.g. test_create_company), that could take a user role and the expected result as inputs:
#[TestWith(['admin', true])]
#[TestWith(['moderator', true])]
#[TestWith(['member', false])]
// And your other roles...
public function test_create_company(string $role, bool $expected): void
{
$user = User::factory()->role($role)->createOne();
$this->assertDatabaseEmpty('companies');
$response = $this->actingAs($user)->post('/companies', [
// Valid company data...
]);
if ($expected) {
$response->assertRedirect()->assertValid();
$this->assertDatabaseHas('companies', [
// Fields and values that should exist in database...
]);
} else {
$response->assertForbidden();
$this->assertDatabaseEmpty('companies');
}
}
So the above test case will create a user with the given role and attempt to create a company. Then, depending on the expected result, will either assert the request was successful and a record was inserted into the database, or assert a 403 Forbidden response was returned and no company records were created.
Once I've tested the authorisation, I'm guessing I should have form validation tests - if there's 10 fields on the page, do I create 10 validation tests? Or one test to cover all 10 fields?
Again, you can use a data provider for say, testing required fields. For most resources in my applications, I‘ll have a test method that looks like this:
#[TestWith(['name'])]
#[TestWith(['description'])]
#[TestWith(['email'])]
#[TestWith(['telephone_number'])]
#[TestWith(['website_url'])]
public function test_field_is_required(string $field): void
{
$user = User::factory()->role('admin')->createOne();
$data = [
'name' => fake()->company(),
'email' => fake()->safeEmail(),
// All other fields with a valid value...
];
Arr::forget($data, $field);
$this
->actingAs($user)
->post('/companies', $data)
->assertInvalid($field);
}
The above test case will then be re-ran multiple times with a different field name provided as an argument each time. The test case has an array of “valid” data, but then removes the named field, and asserts the request throws a validation error for that named field.
ella-stinnes wrote a reply+100 XP
2mos ago
ella-stinnes started a new conversation+100 XP
2mos ago
I'm new to testing so I'm trying to figure out what I should test.
I have a CRUD for managing company details.
The application has 8 different user roles, some users can access the full CRUD, some are read only and others cannot see the company details at all.
Should I be creating 8 tests for the different user roles?
Once I've tested the authorisation, I'm guessing I should have form validation tests - if there's 10 fields on the page, do I create 10 validation tests? Or one test to cover all 10 fields?
Then there's the store/update methods, I'm guessing I assert the database contains the values.
Do you have a testing workflow for a standard CRUD as to what should be covered?
I'm not sure if I'm writing too many tests.
ella-stinnes wrote a reply+100 XP
5mos ago
Thank you, I looked at the parsed view in the framework storage and the query is duplicated on the following two lines:
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'components.input.field-select','data' => ['id' => 'company_id','options' => \App\Models\Company::orderBy('company_name')->pluck('company_name', 'id')->toArray()]] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withAttributes(['id' => 'company_id','options' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(\App\Models\Company::orderBy('company_name')->pluck('company_name', 'id')->toArray())]); ?>
I'm running the latest version of the framework.
ella-stinnes wrote a reply+100 XP
5mos ago
Thank you.
I know the drop down is currently used on the create and edit pages. It could potentially be required when filtering the index page or other areas of the system. Would the query then move to the model instead of the controller in this scenario (when other controllers require the same query)?
ella-stinnes wrote a reply+100 XP
5mos ago
ella-stinnes wrote a reply+100 XP
5mos ago
ella-stinnes started a new conversation+100 XP
6mos ago
I have the following select blade component:
@props([
'id' => '',
'options' => array(),
])
<select name="{{ $id }}" id="{{ $id }}">
@foreach($options as $value => $name)
<option value="{{ $value }}">{{ $name }}</option>
@endforeach
</select>
I'm then using this component as follows:
<x-input.field-select id="company_id" :options="\App\Models\Company::orderBy('name')->pluck('name', 'id')->toArray()" />
When debugging, I'm finding that the query is executing twice, but I don't understand why?
Amending my usage to the following solves the issue, but I'd like to understand why the above doesn't work as I think it looks cleaner:
@php
$options = \App\Models\Company::orderBy('name')->pluck('name', 'id')->toArray();
@endphp
<x-input.field-select id="company_id" :options="$options" />