['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);
}
}