Memoize specialty nav and branch lookups to stop per-request N+1.
Deploy Ladill Care / deploy (push) Successful in 28s
Deploy Ladill Care / deploy (push) Successful in 28s
Sidebar was re-querying practitioners for every specialty module and also calling ensureDefaultModulesProvisioned on each page render, which showed up as multi-second PHP-FPM slow logs on Care. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,6 +7,7 @@ use App\Listeners\PlatformServiceEventListener;
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Care\OrganizationResolver;
|
use App\Services\Care\OrganizationResolver;
|
||||||
use App\Services\Care\PlanService;
|
use App\Services\Care\PlanService;
|
||||||
|
use App\Services\Care\SpecialtyModuleService;
|
||||||
use Illuminate\Cache\RateLimiting\Limit;
|
use Illuminate\Cache\RateLimiting\Limit;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
@@ -18,7 +19,9 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
{
|
{
|
||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
//
|
// Request-scoped memoization for specialty nav / branch scope (avoids N+1 per module).
|
||||||
|
$this->app->singleton(OrganizationResolver::class);
|
||||||
|
$this->app->singleton(SpecialtyModuleService::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ use Illuminate\Support\Str;
|
|||||||
|
|
||||||
class OrganizationResolver
|
class OrganizationResolver
|
||||||
{
|
{
|
||||||
|
/** @var array<string, list<int>> */
|
||||||
|
protected array $doctorAssignedBranchIdsCache = [];
|
||||||
|
|
||||||
|
/** @var array<string, ?Member> */
|
||||||
|
protected array $membershipCache = [];
|
||||||
|
|
||||||
public function resolveForUser(User $user): ?Organization
|
public function resolveForUser(User $user): ?Organization
|
||||||
{
|
{
|
||||||
$member = $this->membershipFor($user);
|
$member = $this->membershipFor($user);
|
||||||
@@ -48,19 +54,24 @@ class OrganizationResolver
|
|||||||
*/
|
*/
|
||||||
public function membershipFor(User $user): ?Member
|
public function membershipFor(User $user): ?Member
|
||||||
{
|
{
|
||||||
|
$cacheKey = $user->ownerRef();
|
||||||
|
if (array_key_exists($cacheKey, $this->membershipCache)) {
|
||||||
|
return $this->membershipCache[$cacheKey];
|
||||||
|
}
|
||||||
|
|
||||||
$member = Member::query()->where('user_ref', $user->ownerRef())->first();
|
$member = Member::query()->where('user_ref', $user->ownerRef())->first();
|
||||||
if ($member) {
|
if ($member) {
|
||||||
return $member;
|
return $this->membershipCache[$cacheKey] = $member;
|
||||||
}
|
}
|
||||||
|
|
||||||
$email = strtolower(trim((string) $user->email));
|
$email = strtolower(trim((string) $user->email));
|
||||||
if ($email === '' || $email === $user->ownerRef()) {
|
if ($email === '' || $email === $user->ownerRef()) {
|
||||||
return null;
|
return $this->membershipCache[$cacheKey] = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$member = Member::query()->where('user_ref', $email)->first();
|
$member = Member::query()->where('user_ref', $email)->first();
|
||||||
|
|
||||||
return $member ? $this->remapMemberUserRef($member, $user) : null;
|
return $this->membershipCache[$cacheKey] = ($member ? $this->remapMemberUserRef($member, $user) : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -291,6 +302,11 @@ class OrganizationResolver
|
|||||||
*/
|
*/
|
||||||
public function doctorAssignedBranchIds(Member $member): array
|
public function doctorAssignedBranchIds(Member $member): array
|
||||||
{
|
{
|
||||||
|
$cacheKey = (string) $member->id;
|
||||||
|
if (isset($this->doctorAssignedBranchIdsCache[$cacheKey])) {
|
||||||
|
return $this->doctorAssignedBranchIdsCache[$cacheKey];
|
||||||
|
}
|
||||||
|
|
||||||
$practitioners = Practitioner::query()
|
$practitioners = Practitioner::query()
|
||||||
->where('is_active', true)
|
->where('is_active', true)
|
||||||
->where(function ($query) use ($member) {
|
->where(function ($query) use ($member) {
|
||||||
@@ -309,10 +325,10 @@ class OrganizationResolver
|
|||||||
$ids = array_values(array_unique(array_map('intval', $ids)));
|
$ids = array_values(array_unique(array_map('intval', $ids)));
|
||||||
|
|
||||||
if ($ids === [] && $member->branch_id) {
|
if ($ids === [] && $member->branch_id) {
|
||||||
return [(int) $member->branch_id];
|
$ids = [(int) $member->branch_id];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $ids;
|
return $this->doctorAssignedBranchIdsCache[$cacheKey] = $ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mayAccessBranch(?Member $member, ?int $branchId): bool
|
public function mayAccessBranch(?Member $member, ?int $branchId): bool
|
||||||
|
|||||||
@@ -21,6 +21,18 @@ use Illuminate\Support\Facades\Log;
|
|||||||
*/
|
*/
|
||||||
class SpecialtyModuleService
|
class SpecialtyModuleService
|
||||||
{
|
{
|
||||||
|
/** @var array<string, \Illuminate\Support\Collection<int, Practitioner>> */
|
||||||
|
protected array $practitionersCache = [];
|
||||||
|
|
||||||
|
/** @var array<string, bool> */
|
||||||
|
protected array $deskSpecialistCache = [];
|
||||||
|
|
||||||
|
/** @var array<string, list<array{key: string, definition: array<string, mixed>, access_level: string}>> */
|
||||||
|
protected array $enabledModulesForMemberCache = [];
|
||||||
|
|
||||||
|
/** @var array<string, string> */
|
||||||
|
protected array $memberAccessLevelCache = [];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected PlanService $plans,
|
protected PlanService $plans,
|
||||||
) {}
|
) {}
|
||||||
@@ -168,6 +180,11 @@ class SpecialtyModuleService
|
|||||||
*/
|
*/
|
||||||
public function enabledModulesForMember(Organization $organization, ?Member $member): array
|
public function enabledModulesForMember(Organization $organization, ?Member $member): array
|
||||||
{
|
{
|
||||||
|
$cacheKey = $organization->id.'|'.($member?->id ?? 'guest');
|
||||||
|
if (isset($this->enabledModulesForMemberCache[$cacheKey])) {
|
||||||
|
return $this->enabledModulesForMemberCache[$cacheKey];
|
||||||
|
}
|
||||||
|
|
||||||
$out = [];
|
$out = [];
|
||||||
foreach ($this->enabledModules($organization) as $item) {
|
foreach ($this->enabledModules($organization) as $item) {
|
||||||
$level = $this->memberAccessLevel($organization, $member, $item['key']);
|
$level = $this->memberAccessLevel($organization, $member, $item['key']);
|
||||||
@@ -177,7 +194,7 @@ class SpecialtyModuleService
|
|||||||
$out[] = array_merge($item, ['access_level' => $level]);
|
$out[] = array_merge($item, ['access_level' => $level]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $out;
|
return $this->enabledModulesForMemberCache[$cacheKey] = $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -223,32 +240,37 @@ class SpecialtyModuleService
|
|||||||
*/
|
*/
|
||||||
public function memberAccessLevel(Organization $organization, ?Member $member, string $key): string
|
public function memberAccessLevel(Organization $organization, ?Member $member, string $key): string
|
||||||
{
|
{
|
||||||
|
$cacheKey = $organization->id.'|'.($member?->id ?? 'guest').'|'.$key;
|
||||||
|
if (isset($this->memberAccessLevelCache[$cacheKey])) {
|
||||||
|
return $this->memberAccessLevelCache[$cacheKey];
|
||||||
|
}
|
||||||
|
|
||||||
if (! $this->isEnabled($organization, $key)) {
|
if (! $this->isEnabled($organization, $key)) {
|
||||||
return 'none';
|
return $this->memberAccessLevelCache[$cacheKey] = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
$definition = $this->definition($key);
|
$definition = $this->definition($key);
|
||||||
if (! $definition || ! $member) {
|
if (! $definition || ! $member) {
|
||||||
return 'none';
|
return $this->memberAccessLevelCache[$cacheKey] = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (app(CarePermissions::class)->isAdmin($member)) {
|
if (app(CarePermissions::class)->isAdmin($member)) {
|
||||||
return 'manage';
|
return $this->memberAccessLevelCache[$cacheKey] = 'manage';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->memberCanManage($organization, $member, $key)) {
|
if ($this->memberCanManage($organization, $member, $key)) {
|
||||||
return 'manage';
|
return $this->memberAccessLevelCache[$cacheKey] = 'manage';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use gated helpers so desk specialists do not inherit GP view/refer on other modules.
|
// Use gated helpers so desk specialists do not inherit GP view/refer on other modules.
|
||||||
if ($this->memberCanRefer($organization, $member, $key)) {
|
if ($this->memberCanRefer($organization, $member, $key)) {
|
||||||
return 'refer';
|
return $this->memberAccessLevelCache[$cacheKey] = 'refer';
|
||||||
}
|
}
|
||||||
if ($this->memberCanView($organization, $member, $key)) {
|
if ($this->memberCanView($organization, $member, $key)) {
|
||||||
return 'view';
|
return $this->memberAccessLevelCache[$cacheKey] = 'view';
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'none';
|
return $this->memberAccessLevelCache[$cacheKey] = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -364,17 +386,22 @@ class SpecialtyModuleService
|
|||||||
*/
|
*/
|
||||||
public function isDeskSpecialist(Organization $organization, Member $member): bool
|
public function isDeskSpecialist(Organization $organization, Member $member): bool
|
||||||
{
|
{
|
||||||
|
$cacheKey = $organization->id.'|'.$member->id;
|
||||||
|
if (isset($this->deskSpecialistCache[$cacheKey])) {
|
||||||
|
return $this->deskSpecialistCache[$cacheKey];
|
||||||
|
}
|
||||||
|
|
||||||
if ((string) $member->role !== 'doctor') {
|
if ((string) $member->role !== 'doctor') {
|
||||||
return false;
|
return $this->deskSpecialistCache[$cacheKey] = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (array_keys($this->catalog()) as $key) {
|
foreach (array_keys($this->catalog()) as $key) {
|
||||||
if ($this->specialistBelongsToModule($organization, $member, $key)) {
|
if ($this->specialistBelongsToModule($organization, $member, $key)) {
|
||||||
return true;
|
return $this->deskSpecialistCache[$cacheKey] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return $this->deskSpecialistCache[$cacheKey] = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -508,9 +535,14 @@ class SpecialtyModuleService
|
|||||||
*/
|
*/
|
||||||
protected function practitionersForMember(Organization $organization, Member $member)
|
protected function practitionersForMember(Organization $organization, Member $member)
|
||||||
{
|
{
|
||||||
|
$cacheKey = $organization->id.'|'.$member->id;
|
||||||
|
if (isset($this->practitionersCache[$cacheKey])) {
|
||||||
|
return $this->practitionersCache[$cacheKey];
|
||||||
|
}
|
||||||
|
|
||||||
$branchId = app(OrganizationResolver::class)->branchScope($member);
|
$branchId = app(OrganizationResolver::class)->branchScope($member);
|
||||||
|
|
||||||
return Practitioner::owned((string) $organization->owner_ref)
|
return $this->practitionersCache[$cacheKey] = Practitioner::owned((string) $organization->owner_ref)
|
||||||
->where('organization_id', $organization->id)
|
->where('organization_id', $organization->id)
|
||||||
->where('is_active', true)
|
->where('is_active', true)
|
||||||
->where(function ($query) use ($member) {
|
->where(function ($query) use ($member) {
|
||||||
|
|||||||
@@ -81,12 +81,9 @@
|
|||||||
// Desk specialists skip this group (redundant single-desk list); dashboard cards remain.
|
// Desk specialists skip this group (redundant single-desk list); dashboard cards remain.
|
||||||
if ($permissions->handlesFloorCare($member) && $organization) {
|
if ($permissions->handlesFloorCare($member) && $organization) {
|
||||||
$specialtyService = app(\App\Services\Care\SpecialtyModuleService::class);
|
$specialtyService = app(\App\Services\Care\SpecialtyModuleService::class);
|
||||||
$specialtyService->ensureDefaultModulesProvisioned(
|
$specialtyModulesForNav = $specialtyService->enabledModulesForMember($organization, $member);
|
||||||
$organization,
|
if ($specialtyModulesForNav !== [] && $specialtyService->shouldShowSpecialtyNav($organization, $member)) {
|
||||||
(string) $organization->owner_ref,
|
foreach ($specialtyModulesForNav as $item) {
|
||||||
);
|
|
||||||
if ($specialtyService->shouldShowSpecialtyNav($organization, $member)) {
|
|
||||||
foreach ($specialtyService->enabledModulesForMember($organization, $member) as $item) {
|
|
||||||
$key = $item['key'];
|
$key = $item['key'];
|
||||||
$label = $item['definition']['nav_label'] ?? $item['definition']['label'] ?? $key;
|
$label = $item['definition']['nav_label'] ?? $item['definition']['label'] ?? $key;
|
||||||
$nav[] = [
|
$nav[] = [
|
||||||
|
|||||||
Reference in New Issue
Block a user