''";
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'] ?? '/');
?>
USLUGI.FR
На iPhone: нажмите (
) Поделиться, затем Добавить на экран.
Sur iPhone : appuie sur Partager (
), puis Sur l'écran d'accueil.
' . 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);
?>
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);
?>