Anatomia de l'index.php: totes les funcions explicades
Aquest index.php és alhora el panell d'administració i la web pública del blog. Combina backend PHP + MySQL amb frontend Tailwind, Alpine.js, Fancybox i Clipboard.js, tot en un sol fitxer d'uns 2.900 línies.
La constant EXPLORER_SCRIPT (nom del fitxer actual) s'utilitza als formularis i redireccions perquè el codi sigui portable si es desplaça o es renombra.
1. Autenticació i sessió
admin_auth_config()— Llegeix credencials d'administració des de variables d'entorn (BLOG_ADMIN_USER,BLOG_ADMIN_PASS_HASH,BLOG_ADMIN_PASS). Si no n'hi ha cap, usaadmin / admin123(només desenvolupament local).authenticate_project_user()— Autentica contra la taulausers(Laravel). Permet login per email o name ambpassword_verify().user_is_identified()— Comprova indicadors de sessió (authenticated,user_id,logged_in…) i capçaleres HTTP per decidir si l'usuari pot editar.inc/portal-auth.php— Autenticació compartida amb altres mòduls del portal; gestionaportal_safe_return_url()per redirigir després del login.
Accions POST: login (primer prova taula users, després fallback admin) i logout (destrueix la sessió).
2. Connexió a la base de dades
posts_adk_connect() obre una connexió mysqli a posts_adk (taules b_*) amb charset utf8mb4. Retorna null si falla.
function posts_adk_connect(): ?mysqli
{
$mysqli = @new mysqli('localhost', 'joan', '****', 'posts_adk');
if ($mysqli->connect_error) {
return null;
}
$mysqli->set_charset('utf8mb4');
return $mysqli;
}
3. Missatges flash (avisos d'èxit)
blog_flash_redirect($message, $type)— Desa un missatge a$_SESSION['blog_flash']i redirigeix a l'index. S'usa després de crear, editar o eliminar posts.blog_consume_flash()— Llegeix i esborra el missatge de la sessió en carregar la pàgina.
El frontend mostra un toast verd (èxit) o vermell (error) durant 5 segons. Les dades del flash viuen en un <script type="application/json"> separat per no trencar l'atribut x-data d'Alpine.js.
4. Auto-generació de contingut (s'executa en carregar)
Aquestes funcions ensure_*_post() mantenen posts de documentació sincronitzats amb el codi. En actualitzar un post existent, només es refresquen excerpt, content i img — no es sobreescriu la categoria ni les etiquetes que l'usuari hagi triat manualment.
sync_people_folders_to_posts()— Escanejaimg/people/i crea un post per cada carpeta nova (només títol).ensure_db_connection_guide_post()— Guia de connexió MySQL amb PHP.ensure_bootstrap5_guide_post()— Post Bootstrap 5 amb plantilla CDN.ensure_lluis_llach_post()— Post dedicat a Lluís Llach.ensure_lm_studio_qwen_post()— Post sobre LM Studio i Qwen.ensure_apache_windows_stack_post()— Documentació Apache + PHP + MySQL + phpMyAdmin a Windows.ensure_nitida_studio_portal_post()— Documentació del portal Nítida Studio.ensure_proxmox_deepseek_web_project_post()— Projecte Proxmox + Ubuntu + DeepSeek.ensure_obsidian_second_brain_post()— Obsidian i segon cervell en Markdown local.ensure_notebooklm_post()— Post sobre NotebookLM (Google).ensure_wordpress_novacompt_169_post()— Demo NovaCompt al CT 105 (192.168.1.169).ensure_powershell_ia_laravel_post()— PowerShell 7, CMD i IA per Laravel local.ensure_n8n_proxmox_lxc_post()— n8n en LXC Proxmox (tteck) i API Token.ensure_index_functions_post()— Aquest mateix post.
5. Etiquetes i categories
ensure_post_tags_by_names()— Assigna etiquetes a un post per nom; crea etiquetes noves ab_etiquetassi cal.syncPostTags()— Sincronitza etiquetes des del formulari (checkboxes + camp «nova etiqueta») ab_post_etiquetas.resolvePostCategoryId()— Resol la categoria del post: si s'omple «nova categoria» la crea o la busca; si no, usa la del desplegable.post_scalar()— Llegeix un valor escalar del POST de forma segura (si arriba com a array, agafa l'últim).
Al formulari d'edició es pot triar categoria existent, crear-ne una de nova, i afegir etiquetes existents o noves.
6. Utilitats d'escapat i imatges
h()— Drecera dehtmlspecialchars(ENT_QUOTES, UTF-8)per evitar XSS.posts_img_url()— Converteix el nom desat a la BBDD en URL pública sotaimg/. Accepta URLs absolutes i rutes que ja comencen perimg/.posts_img_url_versioned()— Afegeix?v=(timestamp del fitxer oupdated_at) per invalidar la caché del navegador.
// A la BBDD només es guarda el nom: "index.jpg".
// posts_img_url() hi afegeix el prefix "img/" per construir la URL pública.
function posts_img_url(?string $path): string
{
$path = trim((string) $path);
if ($path === '') return '';
if (preg_match('#^https?://#i', $path)) return $path;
if (stripos($path, 'img/') === 0) return $path;
return 'img/' . ltrim($path, '/');
}
Regla clau: a b_posts.img només es desa el nom del fitxer (p. ex. index.jpg), mai la ruta completa.
7. Galeries de img/people
post_people_public_url()— URL pública segura sotaimg/people/{carpeta}/…, sense permetre...post_people_gallery_for_post()— Cerca la carpeta deimg/peopleque coincideix amb el títol del post (diverses normalitzacions) i recull imatges, vídeos, àudios i PDFs de forma recursiva. Exclou_thumbnails.
La galeria es carrega via AJAX amb ?action=get_post_details quan s'obre un post.
8. CRUD de posts
Controladors segons $_POST['action'] o $_GET['action']:
| Acció | Mètode | Funció |
|---|---|---|
create_post | POST | Insereix a b_posts + etiquetes |
update_post | POST | Actualitza post + re-sincronitza etiquetes |
delete_post | POST | Esborra post i relacions d'etiquetes |
get_post_details | GET (JSON) | Retorna galeria people i llibres associats |
Funcions de suport:
plainTextToHtml()— Converteix text pla en HTML amb paràgrafs; respecta HTML existent si detecta etiquetes.saveUploadedPostImage()— Puja imatges (jpg, png, gif, webp, bmp, avif) aimg/i retorna només el nom del fitxer.
Després de cada operació CRUD exitosa es crida blog_flash_redirect() amb un missatge d'èxit.
9. Càrrega de dades per a la vista
Abans de pintar l'HTML es consulten:
b_categories— Llista per al desplegable de categories (renderitzat en PHP, no Alpine).b_etiquetas— Totes les etiquetes per al formulari.b_posts+ JOIN amb categories i etiquetes —GROUP_CONCATper agrupar etiquetes per post.
Les dades es passen a Alpine.js com a JSON escapat dins x-data. El filtre per categoria, etiqueta i cerca és reactiu al client.
10. Navegació de projectes del servidor
nav_entry_skip()— Amagavendor,node_modules,.git.is_laravel_project()— Detecta Laravel (artisan,public/index.php,laravel/framework).nav_laravel_test_url()— Generahttp://{carpeta}.test/.nav_vhost_entries()— Llegeixhttpd-vhosts.confper construir el menú de VirtualHosts i Alias locals.
11. Frontend (Alpine.js)
El cos de la pàgina usa un únic bloc x-data amb:
filteredPosts— Filtre per categoria, etiqueta i text.selectedPost— Detall d'un article.editingPost— Modal de crear/editar amb sincronització de categoria (syncCategorySelect,syncCategoryBeforeSubmit).flashNotice— Toast d'èxit/error carregat des del JSON de sessió.initCodeCopy()— Botons «Copiar» als blocs de codi amb Clipboard.js.
12. Taules MySQL principals
| Taula | Ús |
|---|---|
b_posts | Articles del blog |
b_categories | Categories (amb camp svg opcional) |
b_etiquetas | Etiquetes |
b_post_etiquetas | Relació N:N post ↔ etiqueta |
b_books + b_post_books | Llibres associats a posts (detall AJAX) |
users | Usuaris Laravel per al login |
Resum
index.php és un mini-CMS autocontingut: autentica usuaris, genera documentació automàtica, ofereix CRUD complet amb categories, etiquetes i imatges, mostra avisos flash, explora projectes Apache i enriqueix posts «people» amb galeries multimèdia. Tot en un fitxer, sense framework PHP, però amb patrons clars de separació per funcions.