['key' => env('SPLIT_KEY'), 'host' => env('SPLIT_HOST', 'https://ab.pixeltools.ru')], * * // контроллер * public function show(Split $split): View * { * return view('home', ['title' => $split->value('hero', 'title', 'Как сейчас')]); * } * * {{-- шаблон --}} * @variant('hero', 'b') * * @else * * @endvariant * * Вошедший пользователь берётся из auth сам — на всех его устройствах вариант один. */ final class SplitServiceProvider extends ServiceProvider { public function register(): void { // scoped, а не singleton: у каждого запроса свой посетитель, в том числе под Octane $this->app->scoped(Split::class, static function (Application $app): Split { /** @var Request $request */ $request = $app->make('request'); $config = $app->make('config')->get('services.split', []); $key = (string) ($config['key'] ?? ''); // Ключа нет (локально, в тестах) — Сплит выключен: тестов нет, в сеть не ходит if ($key === '') { return new Split('', rules: ['experiments' => []]); } return new Split( siteKey: $key, cookie: $request->cookies->get(Split::COOKIE), userId: $request->user()?->getAuthIdentifier(), query: $request->query(), // Правила живут минуту, а ещё час отдаются устаревшими, пока в фоне не придут свежие // Отказ тоже кэшируем, иначе недоступный Сплит дёргается каждым запросом rulesCache: static fn (Closure $fetch): array => Cache::flexible("split:rules:{$key}", [60, 3600], static function () use ($fetch, $key): array { try { return $fetch(); } catch (Throwable) { return Cache::get("split:rules:{$key}") ?? ['experiments' => []]; } }), host: $config['host'] ?? Split::DEFAULT_HOST, stickyCookie: $request->cookies->get(Split::STICKY_COOKIE), userAgent: $request->userAgent(), acceptLanguage: $request->header('Accept-Language'), referer: Split::externalReferer($request->header('Referer'), $request->getHost()), ); }); } public function boot(): void { // Куку читает и скрипт сайта в браузере: шифровать её нельзя EncryptCookies::except([Split::COOKIE, Split::STICKY_COOKIE]); Blade::if('variant', fn (string $test, string ...$variants): bool => in_array($this->split()->variant($test), $variants, true)); Blade::directive('splitValue', static fn (string $args): string => 'value({$args})); ?>"); // Ключа нет (локально, в тестах) — Сплит выключен: ни куки, ни запросов if (!$this->app->make('config')->get('services.split.key')) { return; } Event::listen(RequestHandled::class, $this->refreshCookie(...)); // Показы и цели — одним запросом, когда посетитель уже получил страницу $this->app->terminating(function (): void { if ($this->app->resolved(Split::class)) { $this->split()->flush(); } }); } /** Кука посетителя ответом сервера: Safari урезает куки из JS до 7 дней, а эту — нет */ private function refreshCookie(RequestHandled $event): void { $known = $event->request->cookies->get(Split::COOKIE); if (!$this->app->resolved(Split::class) && !(is_string($known) && Split::isVisitorId($known))) { return; } $cookie = $this->split()->cookie(); foreach ($cookie === null ? [] : array_filter([Split::COOKIE => $cookie['value'], Split::STICKY_COOKIE => $cookie['sticky']]) as $name => $value) { $event->response->headers->setCookie(Cookie::create( name: $name, value: $value, expire: time() + $cookie['maxAge'], domain: $cookie['domain'], secure: $event->request->isSecure(), httpOnly: false, raw: true, sameSite: Cookie::SAMESITE_LAX, )); } } private function split(): Split { return $this->app->make(Split::class); } }