172 lines
6.2 KiB
PHP
172 lines
6.2 KiB
PHP
|
|
<?php
|
|||
|
|
|
|||
|
|
declare(strict_types=1);
|
|||
|
|
|
|||
|
|
namespace app\common\service;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 中转页浏览器 Pixel 脚本渲染(Facebook / TikTok)
|
|||
|
|
*/
|
|||
|
|
class SplitPixelBrowserRenderer
|
|||
|
|
{
|
|||
|
|
/**
|
|||
|
|
* @param array{facebook: array<int, array<string, mixed>>, tiktok: array<int, array<string, mixed>>} $config
|
|||
|
|
* @return array{
|
|||
|
|
* head_html: string,
|
|||
|
|
* body_html: string,
|
|||
|
|
* track_lines: array<int, string>,
|
|||
|
|
* track_jobs: array<int, array<string, mixed>>
|
|||
|
|
* }
|
|||
|
|
*/
|
|||
|
|
public static function render(array $config): array
|
|||
|
|
{
|
|||
|
|
$fbRows = SplitPixelConfigService::getEnabledSorted($config, SplitPixelConfigService::PLATFORM_FACEBOOK);
|
|||
|
|
$tkRows = SplitPixelConfigService::getEnabledSorted($config, SplitPixelConfigService::PLATFORM_TIKTOK);
|
|||
|
|
|
|||
|
|
$head = [];
|
|||
|
|
$initLines = [];
|
|||
|
|
$trackLines = [];
|
|||
|
|
$jobs = [];
|
|||
|
|
|
|||
|
|
if ($fbRows !== []) {
|
|||
|
|
$head[] = self::facebookLoaderScript();
|
|||
|
|
}
|
|||
|
|
if ($tkRows !== []) {
|
|||
|
|
$head[] = self::tiktokLoaderScript();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
foreach ($fbRows as $row) {
|
|||
|
|
$pixelId = self::escapeJsString((string) ($row['pixel_id'] ?? ''));
|
|||
|
|
$testCode = trim((string) ($row['test_code'] ?? ''));
|
|||
|
|
$event = self::escapeJsString((string) ($row['event'] ?? 'PageView'));
|
|||
|
|
if ($pixelId === '') {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
if ($testCode !== '') {
|
|||
|
|
$testEsc = self::escapeJsString($testCode);
|
|||
|
|
$initLines[] = "if(typeof fbq!=='undefined'){fbq('init','{$pixelId}',{},{test_event_code:'{$testEsc}'});}";
|
|||
|
|
} else {
|
|||
|
|
$initLines[] = "if(typeof fbq!=='undefined'){fbq('init','{$pixelId}');}";
|
|||
|
|
}
|
|||
|
|
$jobs[] = [
|
|||
|
|
'platform' => 'facebook',
|
|||
|
|
'event' => (string) ($row['event'] ?? 'PageView'),
|
|||
|
|
'pixel_id' => (string) ($row['pixel_id'] ?? ''),
|
|||
|
|
];
|
|||
|
|
$trackLines[] = "if(typeof fbq!=='undefined'){fbq('track','{$event}');}";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
foreach ($tkRows as $row) {
|
|||
|
|
$pixelId = self::escapeJsString((string) ($row['pixel_id'] ?? ''));
|
|||
|
|
$event = self::mapTikTokBrowserEvent((string) ($row['event'] ?? 'PageView'));
|
|||
|
|
$eventJs = self::escapeJsString($event);
|
|||
|
|
if ($pixelId === '') {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
$initLines[] = "if(typeof ttq!=='undefined'){ttq.load('{$pixelId}');ttq.page();}";
|
|||
|
|
$jobs[] = [
|
|||
|
|
'platform' => 'tiktok',
|
|||
|
|
'event' => $event,
|
|||
|
|
'pixel_id' => (string) ($row['pixel_id'] ?? ''),
|
|||
|
|
];
|
|||
|
|
$trackLines[] = "if(typeof ttq!=='undefined'){ttq.track('{$eventJs}');}";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return [
|
|||
|
|
'head_html' => implode("\n", $head),
|
|||
|
|
'body_html' => self::wrapScriptBlock($initLines),
|
|||
|
|
'track_lines' => $trackLines,
|
|||
|
|
'track_jobs' => $jobs,
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 跳转 orchestrator:在 setTimeout 回调内触发像素事件后再 replace
|
|||
|
|
*
|
|||
|
|
* @param array<int, string> $trackLines
|
|||
|
|
*/
|
|||
|
|
public static function renderRedirectOrchestrator(string $redirectUrlJson, array $trackLines = [], int $maxWaitMs = 1500): string
|
|||
|
|
{
|
|||
|
|
$trackBlock = $trackLines !== [] ? implode("\n ", $trackLines) : '';
|
|||
|
|
$script = self::renderRedirectOrchestratorScript();
|
|||
|
|
|
|||
|
|
return str_replace(
|
|||
|
|
['{$redirectUrlJson}', '{$maxWaitMs}', '{$trackBlock}'],
|
|||
|
|
[$redirectUrlJson, (string) $maxWaitMs, $trackBlock],
|
|||
|
|
$script
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static function renderRedirectOrchestratorScript(): string
|
|||
|
|
{
|
|||
|
|
return <<<'JS'
|
|||
|
|
<script type="text/javascript">
|
|||
|
|
(function () {
|
|||
|
|
var url = {$redirectUrlJson};
|
|||
|
|
if (!url) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
setTimeout(function () {
|
|||
|
|
{$trackBlock}
|
|||
|
|
window.location.replace(url);
|
|||
|
|
}, {$maxWaitMs});
|
|||
|
|
})();
|
|||
|
|
</script>
|
|||
|
|
JS;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* @param array<int, string> $lines
|
|||
|
|
*/
|
|||
|
|
private static function wrapScriptBlock(array $lines): string
|
|||
|
|
{
|
|||
|
|
if ($lines === []) {
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return '<script type="text/javascript">' . "\n"
|
|||
|
|
. implode("\n", $lines) . "\n"
|
|||
|
|
. '</script>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static function facebookLoaderScript(): string
|
|||
|
|
{
|
|||
|
|
return <<<'HTML'
|
|||
|
|
<script>
|
|||
|
|
!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
|||
|
|
n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
|
|||
|
|
n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;
|
|||
|
|
t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');
|
|||
|
|
</script>
|
|||
|
|
HTML;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static function tiktokLoaderScript(): string
|
|||
|
|
{
|
|||
|
|
return <<<'HTML'
|
|||
|
|
<script>
|
|||
|
|
!function(w,d,t){w.TiktokAnalyticsObject=t;var ttq=w[t]=w[t]||[];ttq.methods=["page","track","identify","instances","debug","on","off","once","ready","alias","group","enableCookie","disableCookie"],ttq.setAndDefer=function(t,e){t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}};for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);ttq.instance=function(t){for(var e=ttq._i[t]||[],n=0;n<ttq.methods.length;n++)ttq.setAndDefer(e,ttq.methods[n]);return e},ttq.load=function(e,n){var i="https://analytics.tiktok.com/i18n/pixel/events.js";ttq._i=ttq._i||{},ttq._i[e]=[],ttq._i[e]._u=i,ttq._t=ttq._t||{},ttq._t[e]=+new Date,ttq._o=ttq._o||{},ttq._o[e]=n||{};var o=document.createElement("script");o.type="text/javascript",o.async=!0,o.src=i+"?sdkid="+e+"&lib="+t;var a=document.getElementsByTagName("script")[0];a.parentNode.insertBefore(o,a)}}(window,document,'ttq');
|
|||
|
|
</script>
|
|||
|
|
HTML;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static function mapTikTokBrowserEvent(string $event): string
|
|||
|
|
{
|
|||
|
|
$map = [
|
|||
|
|
'PageView' => 'Pageview',
|
|||
|
|
'Lead' => 'SubmitForm',
|
|||
|
|
'Contact' => 'Contact',
|
|||
|
|
'AddToCart' => 'AddToCart',
|
|||
|
|
'Purchase' => 'CompletePayment',
|
|||
|
|
'Subscribe' => 'Subscribe',
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
return $map[$event] ?? 'Pageview';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static function escapeJsString(string $value): string
|
|||
|
|
{
|
|||
|
|
return str_replace(['\\', "'"], ['\\\\', "\\'"], $value);
|
|||
|
|
}
|
|||
|
|
}
|