Ефективне управління контекстом за допомогою функції Remember у Laravel

Перекладено ШІ
Оригінал: Laravel News
Оновлено: 19 серпня, 2025
Вам цікаво, як оптимізувати управління даними запитів у вашому Laravel-додатку? У нашій статті розглядаються потужні методи `remember()` та `rememberHidden()`, які спростять обчислення та підвищать ефективність вашого коду. Читайте далі, щоб дізнатися, як реалізувати пам'ять у контексті запитів для ваших сервісів!

Контекстний API Laravel впроваджує потужні методи remember() та rememberHidden(), що спрощують управління даними, обмеженими запитом, завдяки елегантній мемоізації на основі замикань. Ці функції оптимізують ресурсоємні обчислення по всій аплікації з чистим та зрозумілим синтаксисом.

Функція remember() забезпечує автоматичну мемоізацію в межах контексту запиту:

use Illuminate\Support\Facades\Context;

$roles = Context::remember('user-roles', fn() => $this->calculateUserRoles(auth()->user()));

Цей метод автоматично контролює наявність ключа та обчислення, виконуючи замикання лише коли контекстний ключ відсутній.

Розгляньмо архітектуру з декількома сервісами, де дані, обмежені запитом, такі як налаштування організації, контроль доступу та конфігурації функцій, повинні ефективно ділитися між різними шарами аплікації. Ці значення є ресурсоємними, проте часто використовуються протягом життєвого циклу запиту:

<?php

namespace App\Services;

use Illuminate\Support\Facades\Context;
use App\Models\Organization;
use App\Models\User;

class OrganizationContextService
{
    public function getOrganizationSettings(): array
    {
        return Context::remember('org-settings', function () {
            $organization = $this->getCurrentOrganization();

            return [
                'subscription_tier' => $this->getSubscriptionTier($organization),
                'enabled_modules' => $this->getEnabledModules($organization),
                'user_limits' => $this->getUserLimits($organization),
                'integration_settings' => $this->getIntegrationSettings($organization),
            ];
        });
    }

    public function getCurrentUserAccessLevel(): array
    {
        return Context::remember('user-access-level', function () {
            $user = auth()->user();
            $organization = $this->getCurrentOrganization();

            $directPermissions = $user->permissions()
                ->where('organization_id', $organization->id)
                ->pluck('action')
                ->toArray();

            $groupPermissions = $user->groups()
                ->with('permissions')
                ->get()
                ->flatMap(fn($group) => $group->permissions)
                ->pluck('action')
                ->toArray();

            $inheritedPermissions = $this->calculateInheritedPermissions($user, $organization);

            return array_unique(array_merge(
                $directPermissions,
                $groupPermissions,
                $inheritedPermissions
            ));
        });
    }

    public function getSecureCredentials(): array
    {
        return Context::rememberHidden('secure-credentials', function () {
            $organization = $this->getCurrentOrganization();

            return [
                'api_secret' => decrypt($organization->encrypted_api_secret),
                'webhook_token' => $this->generateWebhookToken($organization),
                'internal_key' => $this->getInternalEncryptionKey($organization),
            ];
        });
    }

    public function hasFeatureAccess(string $feature): bool
    {
        $settings = $this->getOrganizationSettings();
        $userAccess = $this->getCurrentUserAccessLevel();

        return in_array($feature, $settings['enabled_modules']) &
               in_array("feature:{$feature}", $userAccess);
    }

    protected function getCurrentOrganization(): Organization
    {
        return Context::remember('current-organization', function () {
            $subdomain = request()->route('organization') ?? request()->getHost();

            return Organization::where('subdomain', $subdomain)
                             ->with(['subscription', 'settings'])
                             ->firstOrFail();
        });
    }

    protected function getSubscriptionTier(Organization $organization): string
    {
        return $organization->subscription->tier ?? 'basic';
    }

    protected function getEnabledModules(Organization $organization): array
    {
        $baseTier = $this->getSubscriptionTier($organization);
        $customModules = $organization->settings->pluck('module')->toArray();

        return array_merge(
            $this->getDefaultModulesForTier($baseTier),
            $customModules
        );
    }

    protected function getUserLimits(Organization $organization): array
    {
        $tier = $this->getSubscriptionTier($organization);

        return match($tier) {
            'enterprise' => ['max_users' => -1, 'storage_gb' => 1000],
            'professional' => ['max_users' => 100, 'storage_gb' => 100],
            'basic' => ['max_users' => 10, 'storage_gb' => 10],
            default => ['max_users' => 5, 'storage_gb' => 1],
        };
    }

    protected function getIntegrationSettings(Organization $organization): array
    {
        return $organization->integrations()
                          ->where('active', true)
                          ->get()
                          ->mapWithKeys(fn($integration) => [
                              $integration->service => $integration->configuration
                          ])
                          ->toArray();
    }

    protected function calculateInheritedPermissions(User $user, Organization $organization): array
    {
        if ($user->is_organization_owner) {
            return ['admin:*', 'user:*', 'billing:*', 'settings:*'];
        }

        if ($user->is_manager) {
            return ['user:manage', 'reports:view', 'settings:view'];
        }

        return ['profile:edit', 'dashboard:view'];
    }

    protected function generateWebhookToken(Organization $organization): string
    {
        return hash_hmac('sha256', $organization->id . now()->timestamp, config('app.key'));
    }

    protected function getInternalEncryptionKey(Organization $organization): string
    {
        return hash('sha256', config('app.key') . $organization->uuid);
    }

    protected function getDefaultModulesForTier(string $tier): array
    {
        return match($tier) {
            'enterprise' => ['analytics', 'integrations', 'advanced_reports', 'api_access'],
            'professional' => ['analytics', 'basic_reports', 'limited_api'],
            'basic' => ['dashboard', 'basic_reports'],
            default => ['dashboard'],
        };
    }
}

class ReportingController extends Controller
{
    public function __construct(
        private OrganizationContextService $contextService
    ) {}

    public function generateReport(Request $request)
    {
        if (!$this->contextService->hasFeatureAccess('advanced_reports')) {
            abort(403, 'Розширена звітність недоступна для поточного тарифу');
        }

        $settings = $this->contextService->getOrganizationSettings();
        $userAccess = $this->contextService->getCurrentUserAccessLevel();

        return view('reports.advanced', compact('settings', 'userAccess'));
    }

    public function exportData(Request $request)
    {
        $credentials = $this->contextService->getSecureCredentials();

        if (!$this->verifyExportPermissions($credentials['internal_key'])) {
            abort(401, 'Недійсні права експорту');
        }

        return $this->processDataExport($request);
    }
}

Функція remember() гарантує, що витратні операції, такі як обчислення прав доступу та підбори організації, виконуються лише один раз за запит, незалежно від частоти доступу. Варіант rememberHidden() підтримує таку ж поведінку кешування, виключаючи чутливі дані з записів логів, що робить його ідеальним для облікових даних і токенів безпеки.

Цей підхід добре працює в шарах посередників, сервісних залежностях та композерах переглядів, де однакові дані контексту потребують кількох точок доступу. Конструкція на основі замикань створює самодокументований код, який чітко вказує на обчислення, що відбуваються під час промахів кешу.