private function fetchFixtureById($fixtureId) { if ($fixtureId <= 0) { return null; } $context = $this->fetch365FixtureContext($fixtureId); return is_array($context['fixture'] ?? null) ? $context['fixture'] : null; } private function fetchFixtureIncidents($fixtureId) { $context = $this->fetch365FixtureContext($fixtureId); if (empty($context)) { return []; } return $this->extract365FixtureIncidents($context); } private function fetchFixtureContextBundle($fixtureId) { $fixtureId = (int) $fixtureId; if ($fixtureId <= 0) { return []; } $context = $this->fetch365FixtureContext($fixtureId); if (empty($context)) { return []; } $fixture = (array) ($context['fixture'] ?? []); $events = $this->extract365FixtureIncidents($context); $statsPayload = $this->fetch365GameStatsPayload($fixtureId); $stats = !empty($statsPayload) ? $this->extract365FixtureStatistics(['payload' => $statsPayload, 'fixture' => $fixture]) : []; if (empty($stats)) { $stats = $this->extract365FixtureStatistics($context); } $lineups = $this->extract365FixtureLineups($context); if (empty($lineups)) { $lastUpdateId = $this->find365LastUpdateId((array) ($context['payload'] ?? [])); if ($lastUpdateId > 0) { $deltaPayload = $this->fetch365GamePayloadWithLastUpdate($fixtureId, $lastUpdateId); if (!empty($deltaPayload)) { $lineups = $this->extract365FixtureLineups([ 'payload' => $deltaPayload, 'fixture' => $fixture, ]); } } } $players = $this->buildFixturePlayersFromLineupsAndEvents($fixture, $lineups, $events); return [ 'events' => $events, 'stats' => $stats, 'lineups' => $lineups, 'players' => $players, ]; } private function fetchFixtureStatistics($fixtureId) { $fixtureId = (int) $fixtureId; if ($fixtureId <= 0) { return []; } $statsPayload = $this->fetch365GameStatsPayload($fixtureId); if (!empty($statsPayload)) { $context = $this->fetch365FixtureContext($fixtureId); $fixture = (array) ($context['fixture'] ?? []); $statsRows = $this->extract365FixtureStatistics(['payload' => $statsPayload, 'fixture' => $fixture]); if (!empty($statsRows)) { return $statsRows; } } $context = $this->fetch365FixtureContext($fixtureId); if (empty($context)) { return []; } return $this->extract365FixtureStatistics($context); } private function fetchHeadToHead($params) { $pair = trim((string) ($params['h2h'] ?? '')); $leagueId = 0; $fixtureContext = []; $fixtureId = (int) ($params['fixture'] ?? $params['id'] ?? 0); if ($pair === '' && $fixtureId > 0) { $fixtureContext = $this->fetch365FixtureContext($fixtureId); $fx = (array) ($fixtureContext['fixture'] ?? $this->fetchFixtureById($fixtureId)); $homeId = (int) arr_get($fx, 'teams.home.id', 0); $awayId = (int) arr_get($fx, 'teams.away.id', 0); $leagueId = (int) arr_get($fx, 'league.id', 0); if ($homeId > 0 && $awayId > 0) { $pair = $homeId . '-' . $awayId; } } if ($pair === '' || strpos($pair, '-') === false) { return []; } [$a, $b] = array_map('intval', explode('-', $pair, 2)); if ($a <= 0 || $b <= 0) { return []; } $rows = []; if ($fixtureId > 0) { $h2hPayload = $this->fetch365GameH2HPayload($fixtureId); if (!empty($h2hPayload)) { $h2hCollected = $this->collect365PayloadContext($h2hPayload); $h2hCompetitors = (array) ($h2hCollected['competitorsById'] ?? []); $h2hCompetitions = (array) ($h2hCollected['competitionsById'] ?? []); foreach ((array) ($h2hCollected['games'] ?? []) as $game) { $competitionId = (int) ($game['competitionId'] ?? $game['competitionNum'] ?? 0); $competitionMeta = (array) ($h2hCompetitions[$competitionId] ?? []); $competitionName = trim((string) ($competitionMeta['name'] ?? $game['competitionDisplayName'] ?? '')); $resolvedLeagueId = $competitionId > 0 ? $this->resolve365PrimaryCompetitionId($competitionId, $competitionName) : $leagueId; $countryNameH2H = (string) ($h2hCollected['countriesById'][(int) ($competitionMeta['countryId'] ?? 0)]['name'] ?? ''); $candidate = $this->normalize365GameFixture( $game, $resolvedLeagueId, $competitionId, $competitionName, $this->resolve365CompetitionLogo($competitionMeta, $competitionId), $h2hCompetitors, $countryNameH2H ); if (!is_array($candidate)) { continue; } $h = (int) arr_get($candidate, 'teams.home.id', 0); $aw = (int) arr_get($candidate, 'teams.away.id', 0); if (($h === $a && $aw === $b) || ($h === $b && $aw === $a)) { $rows[] = $candidate; } } } } if (!empty($fixtureContext)) { foreach ((array) ($this->collect365PayloadContext((array) ($fixtureContext['payload'] ?? []))['games'] ?? []) as $game) { $fixtureContextCountryName = (string) ($fixtureContext['countriesById'][(int) ($fixtureContext['competitionMeta']['countryId'] ?? 0)]['name'] ?? ''); $candidate = $this->normalize365GameFixture( $game, (int) ($fixtureContext['leagueId'] ?? 0), (int) ($fixtureContext['competitionId'] ?? 0), (string) arr_get($fixtureContext, 'competitionMeta.name', ''), $this->resolve365CompetitionLogo((array) ($fixtureContext['competitionMeta'] ?? []), (int) ($fixtureContext['competitionId'] ?? 0)), (array) ($fixtureContext['competitorsById'] ?? []), $fixtureContextCountryName ); if (!is_array($candidate)) { continue; } $h = (int) arr_get($candidate, 'teams.home.id', 0); $aw = (int) arr_get($candidate, 'teams.away.id', 0); if (($h === $a && $aw === $b) || ($h === $b && $aw === $a)) { $rows[] = $candidate; } } } if (count($rows) < max(1, min(20, (int) ($params['last'] ?? 10)))) { $events = $this->fetch365CatalogFixtures( gmdate('Y-m-d', strtotime('-365 days')), gmdate('Y-m-d', strtotime('+14 days')), $leagueId, 0, false ); foreach ($events as $fx) { $h = (int) arr_get($fx, 'teams.home.id', 0); $aw = (int) arr_get($fx, 'teams.away.id', 0); if (!(($h === $a && $aw === $b) || ($h === $b && $aw === $a))) { continue; } $rows[] = $fx; } } usort($rows, static function ($x, $y) { return ((int) arr_get($y, 'fixture.timestamp', 0)) <=> ((int) arr_get($x, 'fixture.timestamp', 0)); }); $deduped = []; $seen = []; foreach ($rows as $row) { $fixtureId = (int) arr_get($row, 'fixture.id', 0); if ($fixtureId > 0 && isset($seen[$fixtureId])) { continue; } if ($fixtureId > 0) { $seen[$fixtureId] = true; } $deduped[] = $row; } $limit = max(1, min(20, (int) ($params['last'] ?? 10))); return array_slice($deduped, 0, $limit); } private function fetchFixtureLineups($fixtureId) { $fixtureId = (int) $fixtureId; if ($fixtureId <= 0) { return []; } $context = $this->fetch365FixtureContext($fixtureId); if (empty($context)) { return []; } $lineups = $this->extract365FixtureLineups($context); if (!empty($lineups)) { return $lineups; } $lastUpdateId = $this->find365LastUpdateId((array) ($context['payload'] ?? [])); if ($lastUpdateId <= 0) { return []; } $deltaPayload = $this->fetch365GamePayloadWithLastUpdate($fixtureId, $lastUpdateId); if (empty($deltaPayload)) { return []; } return $this->extract365FixtureLineups([ 'payload' => $deltaPayload, 'fixture' => (array) ($context['fixture'] ?? []), ]); } private function fetchFixturePlayers($fixtureId) { $context = $this->fetch365FixtureContext($fixtureId); if (empty($context)) { return []; } $lineups = $this->extract365FixtureLineups($context); $events = $this->extract365FixtureIncidents($context); return $this->buildFixturePlayersFromLineupsAndEvents((array) ($context['fixture'] ?? []), $lineups, $events); } private function buildFixtureRounds($params) { $nameHint = strtolower(trim((string) ($params['name_hint'] ?? ''))); $leagueId = $this->normalizeLeagueRuntimeId((int) ($params['league'] ?? 0), $nameHint); $events = $this->fetch365CatalogFixtures( gmdate('Y-m-d', strtotime('-120 days')), gmdate('Y-m-d', strtotime('+30 days')), $leagueId, 0, false ); $rounds = []; foreach ($events as $e) { $comp = (array) ($e['league'] ?? []); $cid = (int) ($comp['id'] ?? 0); $cname = strtolower((string) ($comp['name'] ?? '')); if ($leagueId > 0 && $cid !== $leagueId) { if ($nameHint === '' || strpos($cname, $nameHint) === false) { continue; } } $round = trim((string) arr_get($e, 'league.round', '')); if ($round !== '') { $rounds[$round] = true; } } return array_values(array_keys($rounds)); } private function fetch365FixtureContext($fixtureId) { $fixtureId = (int) $fixtureId; if ($fixtureId <= 0) { return []; } $payload = $this->fetch365GamePayload($fixtureId); if (empty($payload)) { return []; } $collected = $this->collect365PayloadContext($payload); $games = (array) ($collected['games'] ?? []); $game = []; foreach ($games as $candidate) { if ((int) ($candidate['id'] ?? 0) === $fixtureId) { $game = $candidate; break; } } if (empty($game) && !empty($games)) { $game = (array) $games[0]; } if (empty($game)) { return []; } $competitionId = (int) ($game['competitionId'] ?? $game['competitionNum'] ?? 0); $competitionsById = (array) ($collected['competitionsById'] ?? []); $competitorsById = (array) ($collected['competitorsById'] ?? []); $competitionNameHint = $this->extract365Text( ((array) ($competitionsById[$competitionId] ?? []))['name'] ?? ($game['competitionDisplayName'] ?? ''), '' ); $leagueId = $competitionId > 0 ? $this->resolve365PrimaryCompetitionId($competitionId, $competitionNameHint) : 0; $competitionMeta = (array) ($competitionsById[$competitionId] ?? $this->default365CompetitionMetaByLeagueId($leagueId)); $competitionName = trim((string) ($competitionMeta['name'] ?? $game['competitionDisplayName'] ?? '')); $competitionLogo = $this->resolve365CompetitionLogo($competitionMeta, $competitionId); $countryName = (string) (($collected['countriesById'] ?? [])[(int) ($competitionMeta['countryId'] ?? 0)]['name'] ?? ''); $fixture = $this->normalize365GameFixture($game, $leagueId, $competitionId, $competitionName, $competitionLogo, $competitorsById, $countryName); return [ 'payload' => $payload, 'game' => $game, 'competitionId' => $competitionId, 'leagueId' => $leagueId, 'competitionMeta' => $competitionMeta, 'competitorsById' => $competitorsById, 'countriesById' => (array) ($collected['countriesById'] ?? []), 'fixture' => $fixture, ]; } private function extract365FixtureIncidents($context) { $payload = (array) ($context['payload'] ?? []); $fixture = (array) ($context['fixture'] ?? []); $competitorNamesById = []; $contextCompetitors = (array) ($context['competitorsById'] ?? []); foreach ($contextCompetitors as $cid => $node) { $cid = (int) $cid; if ($cid <= 0 || !is_array($node)) { continue; } $nm = $this->extract365Text($node['name'] ?? ($node['shortName'] ?? ($node['longName'] ?? '')), ''); if ($nm !== '') { $competitorNamesById[$cid] = $nm; } } foreach (['home', 'away'] as $side) { $tid = (int) arr_get($fixture, 'teams.' . $side . '.id', 0); $tnm = $this->extract365Text(arr_get($fixture, 'teams.' . $side . '.name', ''), ''); if ($tid > 0 && $tnm !== '' && !isset($competitorNamesById[$tid])) { $competitorNamesById[$tid] = $tnm; } } $playerNamesById = []; $this->walk365Payload($payload, function ($node) use (&$playerNamesById) { if (!is_array($node)) { return; } $playerId = (int) ($node['playerId'] ?? $node['competitorPlayerId'] ?? $node['id'] ?? 0); if ($playerId <= 0) { return; } $name = $this->extract365Text( $node['name'] ?? ($node['shortName'] ?? ($node['displayName'] ?? ($node['fullName'] ?? ($node['playerName'] ?? ($node['competitorPlayerName'] ?? ''))))), '' ); $looksPlayerish = isset($node['shirtNumber']) || isset($node['shirtNum']) || isset($node['number']) || isset($node['jerseyNumber']) || isset($node['position']) || isset($node['positionName']) || isset($node['firstName']) || isset($node['lastName']) || isset($node['competitorPlayerId']) || isset($node['playerId']) || isset($node['athleteId']); if ($looksPlayerish && $name !== '' && !isset($playerNamesById[$playerId])) { $playerNamesById[$playerId] = $name; } }); foreach ([$payload['members'] ?? [], $payload['game']['members'] ?? []] as $memberList) { foreach ((array) $memberList as $m) { $mid = (int) ($m['id'] ?? 0); if ($mid <= 0 || isset($playerNamesById[$mid])) { continue; } $mname = $this->extract365Text($m['name'] ?? ($m['shortName'] ?? ''), ''); if ($mname !== '') { $playerNamesById[$mid] = $mname; } } } $actorHints = []; $this->walk365Payload($payload, function ($node) use (&$actorHints, $fixture, $competitorNamesById) { if (!is_array($node)) { return; } $minute = 0; $timeNode = (array) ($node['time'] ?? []); foreach ([ $node['minute'] ?? null, $node['elapsed'] ?? null, $timeNode['elapsed'] ?? null, $timeNode['minute'] ?? null, $node['gameTime'] ?? null, $node['currentMinute'] ?? null, $node['minuteNum'] ?? null, $node['order'] ?? null, $node['sortOrder'] ?? null, ] as $candidate) { if (is_numeric($candidate)) { $minute = (int) $candidate; break; } } if ($minute <= 0) { return; } $teamId = (int) ($node['competitorId'] ?? $node['teamId'] ?? $node['participantId'] ?? 0); if ($teamId <= 0) { $teamNum = (int) ($node['competitorNum'] ?? $node['teamNum'] ?? 0); if ($teamNum === 1) { $teamId = (int) arr_get($fixture, 'teams.home.id', 0); } elseif ($teamNum === 2) { $teamId = (int) arr_get($fixture, 'teams.away.id', 0); } } if ($teamId <= 0) { return; } $type = $this->extract365Text($node['type'] ?? ($node['eventType'] ?? ($node['incidentType'] ?? ($node['incidentTypeName'] ?? ''))), ''); $detail = $this->extract365Text($node['detail'] ?? ($node['description'] ?? ($node['text'] ?? ($node['title'] ?? ($node['eventDescription'] ?? '')))), ''); $kind = $this->classify365IncidentKind($type, $detail); $playerName = $this->extract365Text( $node['playerName'] ?? ($node['competitorPlayerName'] ?? ($node['scorerName'] ?? ($node['goalScorerName'] ?? ($node['competitorPlayerOutName'] ?? ($node['name'] ?? ''))))), '' ); if ($playerName === '' && isset($node['player']) && is_array($node['player'])) { $playerName = $this->extract365Text($node['player']['name'] ?? ($node['player']['displayName'] ?? ($node['player']['fullName'] ?? '')), ''); } if ($playerName !== '') { $teamName = strtolower(trim((string) ($competitorNamesById[$teamId] ?? ''))); if (strtolower($playerName) !== $teamName) { $actorHints[$minute . '|' . $teamId . '|' . $kind] = $playerName; } } }); $rows = []; $seen = []; $this->walk365Payload($payload, function ($node) use (&$rows, &$seen, $context, $competitorNamesById, $playerNamesById, $actorHints) { $incident = $this->normalize365FixtureIncident($node, $context); if (!$incident) { return; } $teamId = (int) arr_get($incident, 'team.id', 0); $teamName = $this->extract365Text(arr_get($incident, 'team.name', ''), ''); if ($teamName === '' && $teamId > 0 && isset($competitorNamesById[$teamId])) { $incident['team']['name'] = (string) $competitorNamesById[$teamId]; } $playerId = (int) arr_get($incident, 'player.id', 0); $playerName = $this->extract365Text(arr_get($incident, 'player.name', ''), ''); if ($playerName === '' && $playerId > 0 && isset($playerNamesById[$playerId])) { $incident['player']['name'] = (string) $playerNamesById[$playerId]; $playerName = (string) $playerNamesById[$playerId]; } if ($playerName === '') { $kind = $this->classify365IncidentKind((string) arr_get($incident, 'type', ''), (string) arr_get($incident, 'detail', '')); $hintKey = ((int) arr_get($incident, 'time.elapsed', 0)) . '|' . $teamId . '|' . $kind; if (isset($actorHints[$hintKey])) { $incident['player']['name'] = (string) $actorHints[$hintKey]; } } $assistId = (int) arr_get($incident, 'assist.id', 0); $assistName = $this->extract365Text(arr_get($incident, 'assist.name', ''), ''); if ($assistName === '' && $assistId > 0 && isset($playerNamesById[$assistId])) { $incident['assist']['name'] = (string) $playerNamesById[$assistId]; } $playerNameSig = trim((string) arr_get($incident, 'player.name', '')); $assistNameSig = trim((string) arr_get($incident, 'assist.name', '')); $sigParts = [ arr_get($incident, 'time.elapsed', 0), arr_get($incident, 'time.extra', 0), arr_get($incident, 'team.id', 0), arr_get($incident, 'type', ''), arr_get($incident, 'detail', ''), ]; if ($playerNameSig !== '' || $assistNameSig !== '') { $sigParts[] = $playerNameSig; $sigParts[] = $assistNameSig; } $sig = md5(json_encode($sigParts)); if (isset($seen[$sig])) { return; } $seen[$sig] = true; $rows[] = $incident; }); usort($rows, static function ($a, $b) { $aMinute = (int) arr_get($a, 'time.elapsed', 0); $bMinute = (int) arr_get($b, 'time.elapsed', 0); if ($aMinute === $bMinute) { return strcmp((string) arr_get($a, 'type', ''), (string) arr_get($b, 'type', '')); } return $aMinute <=> $bMinute; }); if (empty($rows)) { return []; } $bestByCore = []; foreach ($rows as $row) { $coreKey = md5(json_encode([ (int) arr_get($row, 'time.elapsed', 0), (int) arr_get($row, 'time.extra', 0), (int) arr_get($row, 'team.id', 0), strtolower(trim((string) arr_get($row, 'type', ''))), strtolower(trim((string) arr_get($row, 'detail', ''))), ])); if (!isset($bestByCore[$coreKey])) { $bestByCore[$coreKey] = $row; continue; } $current = (array) $bestByCore[$coreKey]; if ($this->incidentRichnessScore($row) > $this->incidentRichnessScore($current)) { $bestByCore[$coreKey] = $row; } } $rows = array_values($bestByCore); usort($rows, static function ($a, $b) { $aMinute = (int) arr_get($a, 'time.elapsed', 0); $bMinute = (int) arr_get($b, 'time.elapsed', 0); if ($aMinute === $bMinute) { $cmpType = strcmp((string) arr_get($a, 'type', ''), (string) arr_get($b, 'type', '')); if ($cmpType !== 0) { return $cmpType; } return strcmp((string) arr_get($a, 'player.name', ''), (string) arr_get($b, 'player.name', '')); } return $aMinute <=> $bMinute; }); return $rows; } private function fetch365GameStatsPayload($fixtureId) { $fixtureId = (int) $fixtureId; if ($fixtureId <= 0) { return []; } $cacheKey = '365:game:stats:' . $fixtureId; if ($this->cache) { $cached = $this->cache->get($cacheKey); if (is_array($cached)) { return $cached; } $stale = $this->cache->getStale($cacheKey); if (is_array($stale)) { return $stale; } } $query = [ 'appTypeId' => $this->scores365AppTypeId, 'langId' => $this->scores365LangId, 'timezoneName' => $this->timezone, 'userCountryId' => $this->scores365UserCountryId, 'games' => $fixtureId, ]; $json = $this->request365Json($this->scores365Base . '/game/stats/', $query, '/365/game/stats'); if ($this->cache && is_array($json) && !empty($json)) { $this->cache->set($cacheKey, $json, 180); } return is_array($json) ? $json : []; } private function extract365FixtureStatistics($context) { $payload = (array) ($context['payload'] ?? []); $pairs = []; $fixture = (array) ($context['fixture'] ?? []); $homeId = (int) arr_get($fixture, 'teams.home.id', 0); $awayId = (int) arr_get($fixture, 'teams.away.id', 0); $this->walk365Payload($payload, function ($node) use (&$pairs, $homeId, $awayId) { $typeId = (int) ($node['typeId'] ?? 0); $label = $this->extract365Text($node['name'] ?? ($node['type'] ?? ($node['label'] ?? ($node['title'] ?? ($node['statName'] ?? ($node['displayName'] ?? ($node['attribute'] ?? ($node['metric'] ?? ''))))))), ''); if ($typeId === 1 || strcasecmp($label, 'possession') === 0) { $label = 'Ball Possession'; } if ($label === '') { $label = $this->extract365Text($node['statTypeName'] ?? ($node['typeName'] ?? ''), ''); } if ($label === '') { return; } if (isset($node['home']) || isset($node['away']) || isset($node['homeValue']) || isset($node['awayValue']) || isset($node['homeCompetitorValue']) || isset($node['awayCompetitorValue'])) { $this->add365StatPair($pairs, $label, $node['home'] ?? '-', $node['away'] ?? '-'); if (!isset($node['home']) && !isset($node['away'])) { $this->add365StatPair( $pairs, $label, $node['homeValue'] ?? ($node['homeCompetitorValue'] ?? '-'), $node['awayValue'] ?? ($node['awayCompetitorValue'] ?? '-') ); } return; } if (isset($node['values']) && is_array($node['values']) && count($node['values']) >= 2) { $values = array_values((array) $node['values']); $this->add365StatPair($pairs, $label, $values[0] ?? '-', $values[1] ?? '-'); return; } if (isset($node['competitors']) && is_array($node['competitors'])) { $homeValue = null; $awayValue = null; foreach ((array) $node['competitors'] as $statNode) { if (!is_array($statNode)) { continue; } $teamId = (int) ($statNode['competitorId'] ?? $statNode['teamId'] ?? $statNode['id'] ?? 0); $value = $statNode['value'] ?? $statNode['statValue'] ?? $statNode['displayValue'] ?? null; if ($teamId > 0 && $teamId === $homeId) { $homeValue = $value; } elseif ($teamId > 0 && $teamId === $awayId) { $awayValue = $value; } elseif ($homeValue === null) { $homeValue = $value; } else { $awayValue = $value; } } if ($homeValue !== null || $awayValue !== null) { $this->add365StatPair($pairs, $label, $homeValue ?? '-', $awayValue ?? '-'); } } $value = $node['value'] ?? $node['statValue'] ?? $node['displayValue'] ?? $node['val'] ?? null; if ($value === null || $value === '') { return; } $teamId = (int) ($node['competitorId'] ?? $node['teamId'] ?? $node['id'] ?? 0); $teamNum = (int) ($node['competitorNum'] ?? $node['teamNum'] ?? 0); $side = null; if ($teamId > 0 && $teamId === $homeId) { $side = 'home'; } elseif ($teamId > 0 && $teamId === $awayId) { $side = 'away'; } elseif ($teamNum === 1) { $side = 'home'; } elseif ($teamNum === 2) { $side = 'away'; } if ($side !== null) { if (!isset($pairs[$label])) { $pairs[$label] = ['home' => '-', 'away' => '-']; } $pairs[$label][$side] = is_scalar($value) ? (string) $value : '-'; } }); if (empty($pairs)) { return []; } $homeTeam = (array) arr_get($fixture, 'teams.home', []); $awayTeam = (array) arr_get($fixture, 'teams.away', []); $homeStats = []; $awayStats = []; foreach ($pairs as $label => $pair) { $homeStats[] = ['type' => $label, 'value' => (string) ($pair['home'] ?? '-')]; $awayStats[] = ['type' => $label, 'value' => (string) ($pair['away'] ?? '-')]; } return [ ['team' => ['id' => (int) ($homeTeam['id'] ?? 0), 'name' => (string) ($homeTeam['name'] ?? 'Home'), 'logo' => (string) ($homeTeam['logo'] ?? ''), 'winner' => null], 'statistics' => $homeStats], ['team' => ['id' => (int) ($awayTeam['id'] ?? 0), 'name' => (string) ($awayTeam['name'] ?? 'Away'), 'logo' => (string) ($awayTeam['logo'] ?? ''), 'winner' => null], 'statistics' => $awayStats], ]; } private function extract365FixtureLineups($context) { $payload = (array) ($context['payload'] ?? []); $fixture = (array) ($context['fixture'] ?? []); $teams = [ 'home' => (array) arr_get($fixture, 'teams.home', []), 'away' => (array) arr_get($fixture, 'teams.away', []), ]; $rawSets = [ 'home' => ['formation' => '', 'startXI' => [], 'substitutes' => [], 'coach' => []], 'away' => ['formation' => '', 'startXI' => [], 'substitutes' => [], 'coach' => []], ]; $membersById = []; $srcMembers = $payload['members'] ?? ($payload['game']['members'] ?? []); foreach ((array) $srcMembers as $m) { $mid = (int) ($m['id'] ?? 0); if ($mid > 0) { $membersById[$mid] = $m; } } $this->walk365Payload($payload, function ($node) use (&$rawSets, $teams, $membersById) { $node = is_array($node) ? $node : []; if (isset($node['homeLineup']) || isset($node['homeLineups']) || isset($node['homeStartingLineup']) || isset($node['homeStartingXI'])) { $this->assign365RawLineupSet($rawSets, 'home', (array) ($node['homeLineup'] ?? $node['homeLineups'] ?? $node['homeStartingLineup'] ?? $node['homeStartingXI'] ?? []), $membersById); } if (isset($node['awayLineup']) || isset($node['awayLineups']) || isset($node['awayStartingLineup']) || isset($node['awayStartingXI'])) { $this->assign365RawLineupSet($rawSets, 'away', (array) ($node['awayLineup'] ?? $node['awayLineups'] ?? $node['awayStartingLineup'] ?? $node['awayStartingXI'] ?? []), $membersById); } if (isset($node['homeCompetitor']) && is_array($node['homeCompetitor'])) { $this->assign365RawLineupSet($rawSets, 'home', (array) $node['homeCompetitor'], $membersById); } if (isset($node['awayCompetitor']) && is_array($node['awayCompetitor'])) { $this->assign365RawLineupSet($rawSets, 'away', (array) $node['awayCompetitor'], $membersById); } if (isset($node['homeCompetitorLineup']) || isset($node['homeCompetitorLineups'])) { $this->assign365RawLineupSet($rawSets, 'home', (array) ($node['homeCompetitorLineup'] ?? $node['homeCompetitorLineups'] ?? []), $membersById); } if (isset($node['awayCompetitorLineup']) || isset($node['awayCompetitorLineups'])) { $this->assign365RawLineupSet($rawSets, 'away', (array) ($node['awayCompetitorLineup'] ?? $node['awayCompetitorLineups'] ?? []), $membersById); } if (isset($node['lineups']) && is_array($node['lineups'])) { foreach ((array) $node['lineups'] as $idx => $lineupNode) { if (!is_array($lineupNode)) continue; $lnTeam = $this->extract365TeamNode($lineupNode); if ((int)($lnTeam['id'] ?? 0) === (int)($teams['home']['id'] ?? 0)) { $this->assign365RawLineupSet($rawSets, 'home', $lineupNode, $membersById); } elseif ((int)($lnTeam['id'] ?? 0) === (int)($teams['away']['id'] ?? 0)) { $this->assign365RawLineupSet($rawSets, 'away', $lineupNode, $membersById); } else { $num = (int)($lineupNode['competitorNum'] ?? $lineupNode['teamNum'] ?? 0); if ($num === 1 || $idx === 0) $this->assign365RawLineupSet($rawSets, 'home', $lineupNode, $membersById); elseif ($num === 2 || $idx === 1) $this->assign365RawLineupSet($rawSets, 'away', $lineupNode, $membersById); } } } }); $out = []; foreach (['home', 'away'] as $side) { $teamId = (int) ($teams[$side]['id'] ?? 0); $teamName = (string) ($teams[$side]['name'] ?? ucfirst($side)); $startXI = $this->normalize365LineupSet($rawSets[$side]['startXI'], $teamId, $teamName); $subs = $this->normalize365LineupSet($rawSets[$side]['substitutes'], $teamId, $teamName); if (empty($startXI) && empty($subs)) continue; $coachNode = (array) ($rawSets[$side]['coach'] ?? []); $out[] = [ 'team' => ['id' => $teamId, 'name' => $teamName, 'logo' => (string) ($teams[$side]['logo'] ?? '')], 'formation' => (string) ($rawSets[$side]['formation'] ?? ''), 'startXI' => $startXI, 'substitutes' => $subs, 'coach' => ['id' => (int) ($coachNode['id'] ?? 0), 'name' => trim((string)($coachNode['name'] ?? $coachNode['fullName'] ?? ''))], ]; } return $out; } private function find365LastUpdateId($payload) { $payload = is_array($payload) ? $payload : []; $stack = [$payload]; while (!empty($stack)) { $node = array_pop($stack); if (!is_array($node)) continue; foreach (['lastUpdateId', 'latestUpdateId', 'updateId'] as $k) { if (isset($node[$k]) && is_numeric($node[$k])) { $id = (int) $node[$k]; if ($id > 0) return $id; } } foreach ($node as $v) { if (is_array($v)) $stack[] = $v; } } return 0; } private function fetch365GamePayloadWithLastUpdate($fixtureId, $lastUpdateId) { $query = array_merge($this->request365DefaultQuery(), ['gameId' => (int)$fixtureId, 'lastUpdateId' => (int)$lastUpdateId]); return $this->request365Json($this->scores365Base . '/game/', $query, '/365/game-update'); } private function buildFixturePlayersFromLineupsAndEvents($fixture, $lineups, $events) { $rows = []; foreach ((array)$lineups as $lineup) { $team = (array)($lineup['team'] ?? []); $teamId = (int)($team['id'] ?? 0); if ($teamId <= 0) continue; $bucket = []; foreach (['startXI', 'substitutes'] as $group) { foreach ((array)($lineup[$group] ?? []) as $entry) { $player = (array)($entry['player'] ?? []); $playerId = (int)($player['id'] ?? 0); $name = trim((string)($player['name'] ?? '')); if ($name === '') continue; $bucket[$playerId] = [ 'player' => ['id' => $playerId, 'name' => $name, 'photo' => (string)($player['photo'] ?? '')], 'statistics' => [[ 'team' => ['id' => $teamId, 'name' => (string)($team['name'] ?? ''), 'logo' => (string)($team['logo'] ?? '')], 'league' => ['id' => (int)arr_get($fixture, 'league.id', 0), 'name' => (string)arr_get($fixture, 'league.name', ''), 'logo' => (string)arr_get($fixture, 'league.logo', ''), 'season' => (int)date('Y')], 'games' => ['appearences' => 1, 'lineups' => $group === 'startXI' ? 1 : 0, 'minutes' => 0, 'number' => $player['number'] ?? null, 'position' => (string)($player['pos'] ?? '')], 'substitutes' => ['in' => 0, 'out' => 0, 'bench' => $group === 'substitutes' ? 1 : 0], 'goals' => ['total' => 0, 'assists' => 0], 'cards' => ['yellow' => 0, 'red' => 0], ]] ]; } } // Simple event application foreach ((array)$events as $ev) { if ((int)arr_get($ev, 'team.id', 0) !== $teamId) continue; $pid = (int)arr_get($ev, 'player.id', 0); if ($pid > 0 && isset($bucket[$pid])) { $type = strtolower((string)arr_get($ev, 'type', '')); if ($type === 'goal') $bucket[$pid]['statistics'][0]['goals']['total']++; if (str_contains($type, 'card')) $bucket[$pid]['statistics'][0]['cards'][$type === 'red_card' ? 'red' : 'yellow']++; } } $rows[] = ['team' => $team, 'players' => array_values($bucket)]; } return $rows; } private function fetch365GameH2HPayload($fixtureId) { $query = array_merge($this->request365DefaultQuery(), ['gameId' => (int)$fixtureId, 'addMainOdds' => 'true']); return $this->request365Json($this->scores365Base . '/games/h2h/', $query, '/365/games/h2h'); } private function classify365IncidentKind($type, $detail) { $t = strtolower(trim((string)$type)); $d = strtolower(trim((string)$detail)); if ($t === 'goal' || str_contains($d, 'goal')) return 'goal'; if (str_contains($d, 'yellow')) return 'card'; if (str_contains($d, 'red')) return 'card'; if ($t === 'subst' || str_contains($d, 'substitution')) return 'subst'; return 'other'; } private function normalize365FixtureIncident($node, $context) { $node = is_array($node) ? $node : []; if (!isset($node['minute']) && !isset($node['time'])) return null; $type = $this->extract365Text($node['type'] ?? ($node['incidentType'] ?? ''), ''); if ($type === '') return null; return [ 'time' => ['elapsed' => (int)($node['minute'] ?? $node['time']['elapsed'] ?? 0), 'extra' => (int)($node['extraTime'] ?? 0)], 'team' => ['id' => (int)($node['competitorId'] ?? 0), 'name' => ''], 'player' => ['id' => (int)($node['playerId'] ?? 0), 'name' => ''], 'assist' => ['id' => (int)($node['assistPlayerId'] ?? 0), 'name' => ''], 'type' => $type, 'detail' => $this->extract365Text($node['detail'] ?? '', ''), ]; } private function incidentRichnessScore($incident) { $score = 0; if ((int)arr_get($incident, 'player.id', 0) > 0) $score += 2; if (trim((string)arr_get($incident, 'player.name', '')) !== '') $score += 1; if ((int)arr_get($incident, 'assist.id', 0) > 0) $score += 1; return $score; } private function normalize365LineupSet($source, $teamId, $teamName) { $out = []; foreach ((array)$source as $row) { $p = $this->normalize365LineupPlayer($row, $teamId, $teamName); if ($p) $out[] = $p; } return $out; } private function assign365RawLineupSet(&$rawSets, $side, $node, $membersById) { $node = is_array($node) ? $node : []; if (isset($node['formation'])) $rawSets[$side]['formation'] = $node['formation']; if (isset($node['members']) && is_array($node['members'])) { foreach ($node['members'] as $m) { $status = (int)($m['status'] ?? 0); if ($status === 1) $rawSets[$side]['startXI'][] = $m; elseif ($status === 2) $rawSets[$side]['substitutes'][] = $m; } } } private function walk365Payload($node, $visitor) { $stack = [is_array($node) ? $node : []]; while (!empty($stack)) { $curr = array_pop($stack); if (!is_array($curr)) continue; $visitor($curr); foreach ($curr as $v) { if (is_array($v)) $stack[] = $v; } } } private function extract365FixtureOdds($game) { return []; // Simplified for now } private function add365StatPair(&$pairs, $label, $home, $away) { $pairs[$label] = ['home' => $home, 'away' => $away]; } private function normalize365StatLabel($label) { return $label; } }