348 lines
9.8 KiB
PHP
348 lines
9.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Wspólne funkcje cache/upstream menu (api/menu.php + scripts/refresh_menu_cache.php).
|
|
*/
|
|
|
|
if (!function_exists('array_is_list')) {
|
|
/** Polyfill PHP < 8.1 */
|
|
function array_is_list(array $array): bool
|
|
{
|
|
$i = 0;
|
|
foreach ($array as $k => $_) {
|
|
if ($k !== $i++) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function menuCacheDir(): string
|
|
{
|
|
$dir = __DIR__ . '/cache';
|
|
if (!is_dir($dir)) {
|
|
@mkdir($dir, 0755, true);
|
|
}
|
|
|
|
return $dir;
|
|
}
|
|
|
|
function assertMenuCacheWritable(): void
|
|
{
|
|
$dir = menuCacheDir();
|
|
if (!is_dir($dir) || !is_writable($dir)) {
|
|
$user = function_exists('posix_geteuid')
|
|
? ((string) (@posix_getpwuid(posix_geteuid())['name'] ?? posix_geteuid()))
|
|
: get_current_user();
|
|
throw new RuntimeException(
|
|
"Katalog cache nie jest zapisywalny: {$dir} (proces jako: {$user}). "
|
|
. 'Na serwerze: chown -R www-data:www-data api/cache && chmod 775 api/cache'
|
|
);
|
|
}
|
|
}
|
|
|
|
function menuCachePath(string $lang): string
|
|
{
|
|
$safe = preg_replace('/[^a-z0-9_-]/i', '', strtolower($lang)) ?: 'pl';
|
|
|
|
return menuCacheDir() . '/menu_' . $safe . '.json';
|
|
}
|
|
|
|
function menuFingerprintPath(): string
|
|
{
|
|
return menuCacheDir() . '/menu_pl_fingerprint.txt';
|
|
}
|
|
|
|
function readMenuCache(string $cacheFile): ?array
|
|
{
|
|
if (!is_file($cacheFile)) {
|
|
return null;
|
|
}
|
|
|
|
$raw = file_get_contents($cacheFile);
|
|
if ($raw === false || $raw === '') {
|
|
return null;
|
|
}
|
|
|
|
$decoded = json_decode($raw, true);
|
|
|
|
return is_array($decoded) ? $decoded : null;
|
|
}
|
|
|
|
function writeMenuCache(string $cacheFile, array $payload): void
|
|
{
|
|
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
if ($json === false) {
|
|
throw new RuntimeException('Nie udało się zserializować cache: ' . $cacheFile);
|
|
}
|
|
$ok = @file_put_contents($cacheFile, $json, LOCK_EX);
|
|
if ($ok === false) {
|
|
throw new RuntimeException('Permission denied przy zapisie: ' . $cacheFile);
|
|
}
|
|
}
|
|
|
|
function fetchUpstreamMenu(string $url, int $timeout): ?array
|
|
{
|
|
$body = false;
|
|
$httpCode = 0;
|
|
|
|
if (function_exists('curl_init')) {
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_FOLLOWLOCATION => true,
|
|
CURLOPT_TIMEOUT => $timeout,
|
|
CURLOPT_HTTPHEADER => ['Accept: application/json'],
|
|
]);
|
|
$body = curl_exec($ch);
|
|
$errno = curl_errno($ch);
|
|
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
if ($errno !== 0) {
|
|
$body = false;
|
|
}
|
|
} else {
|
|
$context = stream_context_create([
|
|
'http' => [
|
|
'method' => 'GET',
|
|
'timeout' => $timeout,
|
|
'header' => "Accept: application/json\r\n",
|
|
'ignore_errors' => true,
|
|
],
|
|
]);
|
|
$body = @file_get_contents($url, false, $context);
|
|
if (isset($http_response_header[0]) && preg_match('/\s(\d{3})\s/', $http_response_header[0], $m)) {
|
|
$httpCode = (int) $m[1];
|
|
}
|
|
}
|
|
|
|
if ($body === false || ($httpCode > 0 && ($httpCode < 200 || $httpCode >= 300))) {
|
|
return null;
|
|
}
|
|
|
|
$decoded = json_decode($body, true);
|
|
|
|
return is_array($decoded) ? $decoded : null;
|
|
}
|
|
|
|
function mapUpstreamMenu(array $raw, string $lang, string $source): array
|
|
{
|
|
$categories = [];
|
|
foreach ($raw['categories'] ?? [] as $cat) {
|
|
if (!is_array($cat)) {
|
|
continue;
|
|
}
|
|
$id = trim((string) ($cat['id'] ?? ''));
|
|
$name = trim((string) ($cat['name'] ?? ''));
|
|
if ($id === '' || $name === '') {
|
|
continue;
|
|
}
|
|
$categories[] = ['id' => $id, 'name' => $name];
|
|
}
|
|
|
|
$sections = [];
|
|
foreach ($raw['tables'] ?? [] as $table) {
|
|
if (!is_array($table)) {
|
|
continue;
|
|
}
|
|
$sectionName = trim((string) ($table['name'] ?? ''));
|
|
if ($sectionName === '') {
|
|
continue;
|
|
}
|
|
|
|
$items = [];
|
|
foreach ($table['items'] ?? [] as $item) {
|
|
if (!is_array($item)) {
|
|
continue;
|
|
}
|
|
$items[] = [
|
|
'position' => (string) ($item['id'] ?? ''),
|
|
'categoryId' => (string) ($item['category'] ?? ''),
|
|
'tag' => '',
|
|
'image' => (string) ($item['image'] ?? ''),
|
|
'title' => trim((string) ($item['name'] ?? '')),
|
|
'description' => trim((string) ($item['name_extra'] ?? '')),
|
|
'price' => (string) ($item['price'] ?? ''),
|
|
];
|
|
}
|
|
|
|
$sections[] = [
|
|
'categoryName' => $sectionName,
|
|
'items' => $items,
|
|
];
|
|
}
|
|
|
|
$payload = [
|
|
'status' => 'success',
|
|
'lang' => $lang,
|
|
'source' => $source,
|
|
'cachedAt' => gmdate('c'),
|
|
'categories' => $categories,
|
|
'sections' => $sections,
|
|
];
|
|
|
|
$payload['contentFingerprint'] = menuContentFingerprint($payload);
|
|
|
|
return $payload;
|
|
}
|
|
|
|
/**
|
|
* Stabilny fingerprint treści tekstowych (bez cen/obrazków/daty cache).
|
|
*/
|
|
function menuContentFingerprint(array $menu): string
|
|
{
|
|
$parts = [];
|
|
|
|
foreach ($menu['categories'] ?? [] as $cat) {
|
|
if (!is_array($cat)) {
|
|
continue;
|
|
}
|
|
$parts[] = 'c|' . ($cat['id'] ?? '') . '|' . ($cat['name'] ?? '');
|
|
}
|
|
|
|
foreach ($menu['sections'] ?? [] as $section) {
|
|
if (!is_array($section)) {
|
|
continue;
|
|
}
|
|
$catName = (string) ($section['categoryName'] ?? '');
|
|
$parts[] = 's|' . $catName;
|
|
foreach ($section['items'] ?? [] as $item) {
|
|
if (!is_array($item)) {
|
|
continue;
|
|
}
|
|
$parts[] = implode('|', [
|
|
'i',
|
|
(string) ($item['categoryId'] ?? ''),
|
|
(string) ($item['position'] ?? ''),
|
|
(string) ($item['title'] ?? ''),
|
|
(string) ($item['description'] ?? ''),
|
|
$catName,
|
|
]);
|
|
}
|
|
}
|
|
|
|
sort($parts, SORT_STRING);
|
|
|
|
return hash('sha256', implode("\n", $parts));
|
|
}
|
|
|
|
/**
|
|
* Mapa pozycji do tłumaczenia: key => [title, description, categoryName, categoryId, position, ...]
|
|
*/
|
|
function menuCollectTranslatableUnits(array $menu): array
|
|
{
|
|
$units = [];
|
|
$categoryNames = [];
|
|
|
|
foreach ($menu['categories'] ?? [] as $cat) {
|
|
if (!is_array($cat)) {
|
|
continue;
|
|
}
|
|
$id = (string) ($cat['id'] ?? '');
|
|
if ($id === '') {
|
|
continue;
|
|
}
|
|
$categoryNames[$id] = (string) ($cat['name'] ?? '');
|
|
$units['cat:' . $id] = [
|
|
'type' => 'category',
|
|
'id' => $id,
|
|
'text' => (string) ($cat['name'] ?? ''),
|
|
];
|
|
}
|
|
|
|
foreach ($menu['sections'] ?? [] as $sIdx => $section) {
|
|
if (!is_array($section)) {
|
|
continue;
|
|
}
|
|
$catName = (string) ($section['categoryName'] ?? '');
|
|
$units['sec:' . $sIdx] = [
|
|
'type' => 'section',
|
|
'index' => (int) $sIdx,
|
|
'text' => $catName,
|
|
];
|
|
|
|
foreach ($section['items'] ?? [] as $item) {
|
|
if (!is_array($item)) {
|
|
continue;
|
|
}
|
|
$cid = (string) ($item['categoryId'] ?? '');
|
|
$pos = (string) ($item['position'] ?? '');
|
|
$key = 'item:' . $cid . ':' . $pos;
|
|
$units[$key] = [
|
|
'type' => 'item',
|
|
'categoryId' => $cid,
|
|
'position' => $pos,
|
|
'title' => (string) ($item['title'] ?? ''),
|
|
'description' => (string) ($item['description'] ?? ''),
|
|
'hash' => hash('sha256', ($item['title'] ?? '') . "\0" . ($item['description'] ?? '') . "\0" . $catName),
|
|
];
|
|
}
|
|
}
|
|
|
|
return $units;
|
|
}
|
|
|
|
function menuApplyTranslations(array $plMenu, array $translations, string $lang): array
|
|
{
|
|
$out = $plMenu;
|
|
$out['lang'] = $lang;
|
|
$out['source'] = 'ai';
|
|
$out['cachedAt'] = gmdate('c');
|
|
$out['sourceFingerprint'] = $plMenu['contentFingerprint'] ?? menuContentFingerprint($plMenu);
|
|
|
|
foreach ($out['categories'] ?? [] as $i => $cat) {
|
|
if (!is_array($cat)) {
|
|
continue;
|
|
}
|
|
$id = (string) ($cat['id'] ?? '');
|
|
$key = 'cat:' . $id;
|
|
if (isset($translations[$key]['text'])) {
|
|
$out['categories'][$i]['name'] = (string) $translations[$key]['text'];
|
|
}
|
|
}
|
|
|
|
foreach ($out['sections'] ?? [] as $sIdx => $section) {
|
|
if (!is_array($section)) {
|
|
continue;
|
|
}
|
|
$secKey = 'sec:' . $sIdx;
|
|
if (isset($translations[$secKey]['text'])) {
|
|
$out['sections'][$sIdx]['categoryName'] = (string) $translations[$secKey]['text'];
|
|
}
|
|
|
|
foreach ($section['items'] ?? [] as $iIdx => $item) {
|
|
if (!is_array($item)) {
|
|
continue;
|
|
}
|
|
$cid = (string) ($item['categoryId'] ?? '');
|
|
$pos = (string) ($item['position'] ?? '');
|
|
$key = 'item:' . $cid . ':' . $pos;
|
|
if (isset($translations[$key]['title'])) {
|
|
$out['sections'][$sIdx]['items'][$iIdx]['title'] = (string) $translations[$key]['title'];
|
|
}
|
|
if (array_key_exists('description', $translations[$key] ?? [])) {
|
|
$out['sections'][$sIdx]['items'][$iIdx]['description'] = (string) $translations[$key]['description'];
|
|
}
|
|
}
|
|
}
|
|
|
|
$out['contentFingerprint'] = menuContentFingerprint($out);
|
|
|
|
return $out;
|
|
}
|
|
|
|
function loadMenuAiConfig(): ?array
|
|
{
|
|
$path = __DIR__ . '/../config/menu_ai.local.php';
|
|
if (!is_file($path)) {
|
|
return null;
|
|
}
|
|
$cfg = require $path;
|
|
|
|
return is_array($cfg) ? $cfg : null;
|
|
}
|