''"; if ($q !== '') { $cands = city_query_candidates($q); $cands[] = mb_strtolower($q, 'UTF-8'); $cands = array_values(array_unique($cands)); $ors = []; foreach ($cands as $i => $cand) { $ors[] = "LOWER(ville) LIKE :q$i"; $params[":q$i"] = $cand . '%'; } if ($ors) { $sql .= " AND (" . implode(' OR ', $ors) . ")"; } } $sql .= " ORDER BY val ASC LIMIT 15"; } else { $sql = "SELECT DISTINCT val FROM ( SELECT specialite AS val FROM praticien WHERE specialite IS NOT NULL AND specialite <> '' UNION ALL SELECT CONCAT(prenom, ' ', nom) AS val FROM praticien WHERE prenom IS NOT NULL AND prenom <> '' AND nom IS NOT NULL AND nom <> '' UNION ALL SELECT prenom AS val FROM praticien WHERE prenom IS NOT NULL AND prenom <> '' UNION ALL SELECT nom AS val FROM praticien WHERE nom IS NOT NULL AND nom <> '' ) t WHERE 1"; if ($q !== '') { $sql .= " AND LOWER(val) LIKE :q"; $params[':q'] = mb_strtolower($q, 'UTF-8') . '%'; } $sql .= " ORDER BY (LOWER(val) LIKE :qprefix) DESC, val ASC LIMIT 15"; $params[':qprefix'] = ($q !== '') ? (mb_strtolower($q, 'UTF-8') . '%') : ''; } $stmt = $pdo->prepare($sql); $stmt->execute($params); echo json_encode($stmt->fetchAll(PDO::FETCH_COLUMN)); exit; } /* ========================= Helpers ========================= */ function h($v): string {return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');} $reqUri = h($_SERVER['REQUEST_URI'] ?? '/'); ?> <?php echo h($translations['seo_index_title'] ?? '') ?>


' . h($label) . ''; } }; ?>


query(" SELECT id FROM praticien WHERE photo IS NOT NULL AND photo <> '' AND date_expiration > DATE_ADD(NOW(), INTERVAL 2 DAY) ORDER BY id DESC LIMIT 50 ")->fetchAll(PDO::FETCH_COLUMN); if (! $ids) { throw new Exception('Aucun praticien'); } /* ====================================================== 2️⃣ On mélange et on prend 4 ====================================================== */ shuffle($ids); $ids = array_slice($ids, 0, 4); /* ====================================================== 3️⃣ On récupère les 4 profils ====================================================== */ $placeholders = implode(',', array_fill(0, count($ids), '?')); $stmt = $pdo->prepare(" SELECT id, nom, prenom, specialite, photo FROM praticien WHERE id IN ($placeholders) "); $stmt->execute($ids); $randomPros = $stmt->fetchAll(PDO::FETCH_ASSOC); if (! $randomPros) { throw new Exception('Aucun praticien'); } /* ====================================================== 3️⃣ Notes ====================================================== */ function percent_from_5($note) { $n = max(0, min(5, (float) $note)); return (int) round($n / 5 * 100); } $ratingsById = []; $stRatings = $pdo->prepare(" SELECT praticien_id, ROUND(AVG(note),1) AS m, COUNT(*) AS c FROM avis_praticien WHERE statut='publie' AND praticien_id IN ($placeholders) GROUP BY praticien_id "); $stRatings->execute($ids); foreach ($stRatings->fetchAll(PDO::FETCH_ASSOC) as $r) { $ratingsById[(int) $r['praticien_id']] = [ 'm' => (float) $r['m'], 'c' => (int) $r['c'], ]; } /* ====================================================== 4️⃣ Affichage ====================================================== */ foreach ($randomPros as $rp): $pid = (int) $rp['id']; $photoName = ltrim((string) $rp['photo'], '/'); $rpImg = "/public/uploads/" . $photoName; $abs = APP_HTDOCS . "/public/uploads/" . $photoName; if (! $photoName || ! is_file($abs)) { $rpImg = "/public/uploads/img/user_placeholder.png"; } $fullName = trim(($rp['prenom'] ?? '') . ' ' . ($rp['nom'] ?? '')); $spec = trim((string) ($rp['specialite'] ?? '')); $detailUrl = "/detail_public.php?id=" . $pid; $hour = random_int(9, 18); $availability = ($translations['home_online_at'] ?? 'Онлайн:') . ' ' . sprintf('%02d:00', $hour); $stats = $ratingsById[$pid] ?? ['m' => 0, 'c' => 0]; $note = (float) $stats['m']; $count = (int) $stats['c']; $w = percent_from_5($note); ?>
<?php echo h($fullName) ?>
0): ?>
★★★★★
★★★★★
/5 ()
📅





true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_CONNECTTIMEOUT => $timeout, CURLOPT_TIMEOUT => $timeout, CURLOPT_USERAGENT => 'uslugi.fr (+https://staging.uslugi.fr)', CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, ]); $body = (string) curl_exec($ch); $err = curl_error($ch); $http = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); $ok = ($err === '' && $http >= 200 && $http < 300 && $body !== ''); return [$ok, $body, $err, $http]; } function decode_entities(string $s): string { // ENT_HTML5 gère correctement les entités modernes return html_entity_decode($s, ENT_QUOTES | ENT_HTML5, 'UTF-8'); } function fetch_sovetnik_articles(int $limit = 4, bool $debug = false): array { $cacheFile = APP_HTDOCS . '/cache/sovetnik_feed.json'; $lockFile = APP_HTDOCS . '/cache/sovetnik_feed.lock'; // Soft TTL: on essaye de rafraîchir après 15 min // Hard TTL: on accepte d'afficher un cache jusqu'à 24h (pour ne jamais bloquer la page) $ttlSoft = 15 * 60; $ttlHard = 24 * 60 * 60; $now = time(); $hasCache = is_file($cacheFile); $age = $hasCache ? ($now - filemtime($cacheFile)) : PHP_INT_MAX; // Helper pour lire cache $readCache = function () use ($cacheFile, $limit): array { $data = json_decode((string) @file_get_contents($cacheFile), true); return (is_array($data) && $data) ? array_slice($data, 0, $limit) : []; }; // 1) Cache frais => retour immédiat (rapide) if ($hasCache && $age < $ttlSoft) { return $readCache(); } // 2) Cache expiré mais pas trop vieux => on le garde en "stale" (fallback instantané) $stale = []; if ($hasCache && $age < $ttlHard) { $stale = $readCache(); } // 3) On tente un refresh, MAIS sans bloquer longtemps // + lock pour éviter que plusieurs requêtes refreshent en même temps $canRefresh = true; if (is_file($lockFile)) { // lock encore récent => on ne refresh pas if (($now - filemtime($lockFile)) < 60) { $canRefresh = false; } } if (! $canRefresh) { // On sert le stale cache (ou vide) return $stale; } // On pose le lock (best-effort) @is_dir(dirname($lockFile)) || @mkdir(dirname($lockFile), 0755, true); @file_put_contents($lockFile, (string) $now); // Timeout court = clé de la perf $timeout = 1; // 1) RSS $feedCandidates = [ 'https://www.sovetnik.fr/feed/', 'https://www.sovetnik.fr/?feed=rss2', 'https://www.sovetnik.fr/feed/rss2/', ]; foreach ($feedCandidates as $feedUrl) { [$ok, $xmlStr, $err, $http] = fetch_url($feedUrl, $timeout); if ($debug) { echo "\n\n"; } if (! $ok) { continue; } libxml_use_internal_errors(true); $xml = simplexml_load_string($xmlStr); if (! $xml || empty($xml->channel->item)) { continue; } $out = []; foreach ($xml->channel->item as $item) { $titleRaw = (string) $item->title; $descRaw = (string) $item->description; $out[] = [ 'url' => (string) $item->link, 'title' => trim(decode_entities(strip_tags($titleRaw))), 'desc' => trim(decode_entities(strip_tags($descRaw))), 'img' => '', ]; if (count($out) >= $limit) { break; } } if ($out) { @is_dir(dirname($cacheFile)) || @mkdir(dirname($cacheFile), 0755, true); @file_put_contents($cacheFile, json_encode($out, JSON_UNESCAPED_UNICODE)); @unlink($lockFile); return $out; } } // 2) Fallback API $apiUrl = 'https://www.sovetnik.fr/wp-json/wp/v2/posts?per_page=' . (int) $limit . '&_embed=1'; [$ok, $jsonStr, $err, $http] = fetch_url($apiUrl, $timeout); if ($debug) { echo "\n\n"; } if ($ok) { $arr = json_decode($jsonStr, true); if (is_array($arr)) { $out = []; foreach ($arr as $p) { $title = trim(decode_entities(strip_tags($p['title']['rendered'] ?? ''))); $desc = trim(decode_entities(strip_tags($p['excerpt']['rendered'] ?? ''))); $img = ''; if (! empty($p['_embedded']['wp:featuredmedia'][0]['source_url'])) { $img = (string) $p['_embedded']['wp:featuredmedia'][0]['source_url']; } $out[] = [ 'url' => (string) ($p['link'] ?? ''), 'title' => $title, 'desc' => $desc, 'img' => $img, ]; if (count($out) >= $limit) { break; } } if ($out) { @is_dir(dirname($cacheFile)) || @mkdir(dirname($cacheFile), 0755, true); @file_put_contents($cacheFile, json_encode($out, JSON_UNESCAPED_UNICODE)); @unlink($lockFile); return $out; } } } // Échec refresh => on enlève le lock et on sert le stale @unlink($lockFile); return $stale; } // Debug optionnel $debugArticles = isset($_GET['debug']) && $_GET['debug'] === '1'; $articles = fetch_sovetnik_articles(4, $debugArticles); ?>