Cicle formatiu: Administració de Sistemes Informàtics en Xarxa (ASIX)
Durada: 99 h (66 h centre + 33 h empresa)
Entorn: Ubuntu Server 26.04
Un llenguatge de guions de servidor (server-side scripting language) és un llenguatge de programació que s’executa al servidor web, no al navegador del client. Quan un usuari sol·licita una pàgina, el servidor executa el codi, genera HTML dinàmic i l’envia al client. El client mai no veu el codi font del servidor, només el resultat HTML.
| Llenguatge | Característiques principals | Usos habituals |
|---|---|---|
| PHP | Codi obert, molt estès, integrat en Apache/Nginx, orientat al web | WordPress, Drupal, Laravel |
| Python | Llegible, gran ecosistema (Django, Flask, FastAPI) | Aplicacions web, data science |
| Node.js | JavaScript al servidor, arquitectura basada en esdeveniments, no bloquejant | APIs REST, aplicacions en temps real |
| Java | Robust, tipat, gran rendiment (Spring Boot, Jakarta EE) | Aplicacions empresarials |
| Ruby | Expressiu, convencions sobre configuració (Ruby on Rails) | Prototips, startups |
| ASP.NET | Ecosistema Microsoft, C# o VB.NET | Entorns corporatius Windows |
| Go | Compilat, molt ràpid, concurrent | Microserveis, APIs d’alt rendiment |
PHP (Hypertext Preprocessor) és el llenguatge de servidor més usat al web per diverses raons:
PHP es pot barrejar directament amb HTML. El servidor processa el codi PHP i retorna HTML pur al client:
<!DOCTYPE html>
<html lang="ca">
<head>
<meta charset="UTF-8">
<title><?php echo htmlspecialchars($titol); ?></title>
</head>
<body>
<h1><?= $titol ?></h1>
<?php if ($usuari_connectat): ?>
<p>Benvingut, <?= htmlspecialchars($nom_usuari) ?>!</p>
<?php else: ?>
<p><a href="login.php">Inicia sessió</a></p>
<?php endif; ?>
<ul>
<?php foreach ($articles as $article): ?>
<li><?= htmlspecialchars($article['titol']) ?></li>
<?php endforeach; ?>
</ul>
</body>
</html>El client (navegador) rep únicament el resultat HTML generat, mai el codi PHP.
PHP pot generar CSS dinàmic o injectar classes i estils condicionals:
<!DOCTYPE html>
<html>
<head>
<!-- Generar CSS dinàmic -->
<style>
body { background-color: <?= $color_fons ?>; }
</style>
<!-- O carregar un full d'estils diferent segons l'usuari -->
<link rel="stylesheet" href="<?= $tema === 'fosc' ? 'dark.css' : 'light.css' ?>">
</head>
<body>
<!-- Injectar classes dinàmiques -->
<div class="article <?= $article['destacat'] ? 'destacat' : '' ?>">
<?= htmlspecialchars($article['titol']) ?>
</div>
</body>
</html>PHP pot generar codi JavaScript o passar dades a JS via JSON:
<!DOCTYPE html>
<html>
<body>
<?php
$dades_php = [
'usuari' => 'Joan',
'rol' => 'admin',
'permisos' => ['llegir', 'escriure']
];
?>
<script>
// Passar dades de PHP a JavaScript
const dadesUsuari = <?= json_encode($dades_php, JSON_HEX_TAG) ?>;
console.log(dadesUsuari.usuari); // Joan
// Generar JavaScript condicional
<?php if ($mostrar_popup): ?>
alert('Benvingut de nou!');
<?php endif; ?>
</script>
</body>
</html>PHP processa les dades enviades pels formularis HTML (vegeu la secció 9).
En projectes més grans, és recomanable separar:
index.php ← Controlador: rep la petició, crida la lògica, decideix la vista
models/usuari.php ← Model: lògica de negoci, accés a BD
views/inici.php ← Vista: HTML amb mínim PHP (sols echo i if)
config/bd.php ← Configuració: credencials, constants<?php
// Etiqueta estàndard (sempre vàlida)
echo "Hola";
?>
<?= "Drecera per a echo" ?>Usa sempre
<?phpi<?=. Les etiquetes curtes<?soles estan desactivades per defecte.
Si un fitxer és 100% PHP (sense HTML), omet l’etiqueta de tancament?>per evitar espais en blanc involuntaris que podrien causar errors amb les capçaleres HTTP.
;).echo, ECHO, Echo →
equivalent).$nom ≠ $Nom).<?php
$txt = "Hello world!"; // string
$x = 5; // int
$y = 10.5; // float
$actiu = true; // bool
$buit = null; // null
$prefix = "in"; // string
// Interpolació en cometes dobles
echo "El valor és $x";
echo "Paraula: {$prefix}tolerant"; // claus per delimitar
// Cometes simples: NO interpola
echo 'El valor és $x'; // literal: El valor és $x
?><?php
$global = "Soc global";
function demostracioScope() {
// $global NO és accessible aquí sense declarar-la
// echo $global; // error
// Per accedir a una variable global:
global $global;
echo $global;
// Variable local estàtica (conserva el valor entre crides)
static $comptador = 0;
$comptador++;
echo $comptador;
}
demostracioScope(); // 1
demostracioScope(); // 2
?>Usar global dins funcions és una mala pràctica. Millor
passar-la com a paràmetre.
| Tipus | Exemple | Notes |
|---|---|---|
string |
"Hola" |
Mutable, implementada com array de bytes |
int |
42, 0xFF, 0b1010 |
32 o 64 bits, amb signe |
float |
3.14, 1.5e3 |
Igual que double |
bool |
true, false |
|
array |
[1, 2, 3] |
Array associativa generalment |
object |
new Classe() |
|
null |
null |
Variable no assignada |
resource |
fopen(...) |
Referència a recurs extern |
Truthiness — avaluació com a booleà:
| Valor | Avalua com |
|---|---|
'', '0', 0, 0.0, null, [] |
false |
| Qualsevol altre valor | true |
<?php
define("IVA", 0.21);
define("APP_NOM", "La meva app");
const VERSIO = "2.0";
echo IVA; // 0.21
echo ANIMALS[1]; // cat (arrays a PHP 7+)
define('ANIMALS', ['dog', 'cat', 'bird']);
// Constants màgiques
echo __LINE__; // número de línia
echo __FILE__; // ruta completa del fitxer
echo __DIR__; // directori del fitxer
echo __FUNCTION__; // nom de la funció actual
echo __CLASS__; // nom de la classe actual
?>| Eina | Tipus | Característiques |
|---|---|---|
| Visual Studio Code | Editor lleuger | Extensió PHP Intelephense, debugging integrat, Git, terminal |
| PhpStorm | IDE complet | Autocompleció avançada, refactoring, debugging natiu, BD integrada |
| Sublime Text | Editor lleuger | Ràpid, connectors PHP disponibles |
| NetBeans | IDE | Suport PHP integrat, gratuït |
| Vim / Neovim | Editor terminal | Alta productivitat un cop après, ideal per a servidors remots |
Extensions útils:
Fitxer .vscode/settings.json recomanat:
{
"php.validate.executablePath": "/usr/bin/php",
"editor.formatOnSave": true,
"[php]": {
"editor.defaultFormatter": "bmewburn.vscode-intelephense-client"
},
"intelephense.environment.phpVersion": "8.2.0"
}Les pràctiques d’aquest mòdul es faran sobre una màquina virtual amb Ubuntu 26.04 LTS i un servidor LAMP (Linux + Apache + MariaDB + PHP). Aquesta és la configuració de referència per a totes les activitats.
Instal·lació de l’entorn LAMP a Ubuntu 26.04:
# Actualitzar el sistema
sudo apt update && sudo apt upgrade -y
# Instal·lar Apache
sudo apt install apache2 -y
sudo systemctl enable apache2
sudo systemctl start apache2
# Instal·lar MariaDB
sudo apt install mariadb-server -y
sudo systemctl enable mariadb
sudo systemctl start mariadb
sudo mysql_secure_installation # configuració inicial de seguretat
# Instal·lar PHP i els mòduls necessaris per a WordPress i projectes propis
sudo apt install php libapache2-mod-php php-mysql php-mbstring \
php-xml php-curl php-zip php-gd php-intl php-bcmath php-imagick -y
# Reiniciar Apache per carregar el mòdul PHP
sudo systemctl restart apache2Directori arrel del servidor web:
/var/www/html/ ← aquí es posen els fitxers PHP dels projectesComprovar les versions instal·lades:
apache2 -v # versió d'Apache
php -v # versió de PHP
mariadb --version # versió de MariaDBGestió dels serveis:
sudo systemctl start|stop|restart|status apache2
sudo systemctl start|stop|restart|status mariadbVerificar la instal·lació de PHP al navegador:
Crea el fitxer /var/www/html/info.php amb:
<?php phpinfo(); ?>Accedeix a http://localhost/info.php. Esborra’l un cop
verificat per seguretat.
Per veure les extensions carregades:
<?php print_r(get_loaded_extensions()); ?>Servidor integrat de PHP (per a proves ràpides puntuals):
php -S localhost:8080Aritmètics:
$a = 10; $b = 3;
echo $a + $b; // 13
echo $a - $b; // 7
echo $a * $b; // 30
echo $a / $b; // 3.333... (NOTA: no és divisió entera com a Java o Python)
echo $a % $b; // 1 (mòdul)
echo $a ** $b; // 1000 (exponent)Assignació:
$x = 10;
$x += 5; // 15
$x -= 3; // 12
$x *= 2; // 24
$x /= 4; // 6
$x %= 4; // 2
$x **= 3; // 8
$text .= "!"; // concatenació i assignacióComparació:
var_dump(10 == "10"); // true (igual valor; converteix tipus)
var_dump(10 === "10"); // false (idèntic: valor I tipus)
var_dump(10 != "10"); // false
var_dump(10 !== "10"); // true
// Spaceship (PHP 7+): retorna -1, 0 o 1
echo 1 <=> 2; // -1
echo 2 <=> 2; // 0
echo 3 <=> 2; // 1Preferiu sempre === sobre ==.
Lògics:
// && i || tenen més precedència que =
// and i or en tenen menys (pot causar confusió)
var_dump(true && false); // false
var_dump(true || false); // true
var_dump(!true); // false
var_dump(true xor false); // trueAssignació condicional:
// Ternari
$r = ($condicio) ? 'Sí' : 'No';
// Elvis (PHP 5.3+): retorna esquerra si és truthy
$nom = $valor ?: 'Anònim';
// Null coalescing (PHP 7+): retorna esquerra si no és null
$nom = $_GET['nom'] ?? 'Anònim';
// Null coalescing assignment (PHP 7.4+)
$array['clau'] ??= 'valor per defecte';Condicionals:
if ($nota >= 90) {
echo "Excel·lent";
} elseif ($nota >= 70) {
echo "Notable";
} elseif ($nota >= 50) {
echo "Aprovat";
} else {
echo "Suspès";
}
// switch
switch ($color) {
case "red": echo "Vermell"; break;
case "blue": echo "Blau"; break;
default: echo "Altre";
}
// match (PHP 8+): comparació estricta (===), retorna valor
$missatge = match($codi_http) {
200, 201 => "Èxit",
404 => "No trobat",
500 => "Error del servidor",
default => "Codi desconegut"
};Sintaxi alternativa (per barrejar amb HTML):
<?php if ($condicio): ?>
<p>HTML si cert</p>
<?php elseif ($altra): ?>
<p>HTML si altra condició</p>
<?php else: ?>
<p>HTML altrament</p>
<?php endif; ?>
<?php foreach ($items as $item): ?>
<li><?= htmlspecialchars($item) ?></li>
<?php endforeach; ?>Bucles:
// while
while ($i <= 5) { echo $i++; }
// do...while (sempre executa almenys una vegada)
do { echo $i++; } while ($i < 5);
// for (treure count() fora per eficiència)
for ($i = 0, $n = count($arr); $i < $n; $i++) {
echo $arr[$i];
}
// foreach (la forma més habitual per arrays)
foreach ($colors as $color) { echo $color; }
foreach ($edat as $nom => $anys) { echo "$nom: $anys"; }
// Modificar valors per referència
foreach ($arr as &$valor) { $valor *= 2; }
unset($valor); // IMPORTANT: trencar la referència
// break i continue
for ($i = 0; $i < 10; $i++) {
if ($i == 5) break; // surt del bucle
if ($i % 2 == 0) continue; // salta iteració
echo $i;
}<?php
// Indexades
$fruits = ["poma", "pera", "plàtan"];
$fruits[] = "cirera"; // afegir al final
echo count($fruits); // 4
// Associatives
$persona = ["nom" => "Joan", "edat" => 30, "ciutat" => "BCN"];
echo $persona["nom"]; // Joan
// Multidimensionals
$alumnes = [
["nom" => "Joan", "nota" => 8.5],
["nom" => "Maria", "nota" => 9.0],
];
echo $alumnes[0]["nom"]; // Joan
// Unpacking (PHP 7.4+)
$arr1 = [1, 2, 3];
$arr2 = [0, ...$arr1, 4]; // [0, 1, 2, 3, 4]
// Destructuring (PHP 7.1+)
[$a, $b, $c] = [10, 20, 30];
echo $a; // 10
?>Funcions d’array principals:
sort($arr); // ordena ascendent (reindexa)
rsort($arr); // ordena descendent
asort($arr); // ordena per valor (conserva claus)
ksort($arr); // ordena per clau
usort($arr, fn($a,$b) => $a - $b); // ordenació personalitzada
array_push($arr, $val); // afegeix al final
array_pop($arr); // elimina i retorna l'últim
array_shift($arr); // elimina i retorna el primer
array_unshift($arr, $v); // afegeix al principi
in_array($val, $arr); // comprova si existeix el valor
array_key_exists($k,$a); // comprova si existeix la clau
array_search($val, $arr);// retorna la clau del valor trobat
array_merge($a, $b); // combina dues arrays
array_unique($arr); // elimina duplicats
array_slice($arr, 1, 3); // subconjunt
array_filter($arr, fn($v) => $v > 0); // filtra per condició
array_map(fn($v) => $v*2, $arr); // transforma cada element
array_reduce($arr, fn($acc,$v) => $acc+$v, 0); // redueix a un valor<?php
// Comentari d'una sola línia
# Comentari d'una sola línia (estil Unix/shell)
/*
* Comentari de múltiples línies.
* Útil per a blocs de codi o documentació.
*/
/**
* Comentari DocBlock (estil PHPDoc).
* Usat per documentar funcions, classes i mètodes.
*
* @param string $nom El nom de l'usuari
* @param int $edat L'edat de l'usuari
* @return string Missatge de salutació
*/
function saluda(string $nom, int $edat): string {
return "Hola $nom, tens $edat anys.";
}
?>Strings:
strlen($s) // longitud en bytes
mb_strlen($s, 'UTF-8') // longitud en caràcters (recomanat per UTF-8)
strtolower($s) / strtoupper($s)
trim($s) / ltrim($s) / rtrim($s)
str_replace($cerca, $sub, $s)
strpos($s, $needle) // posició primera ocurrència (o false)
substr($s, $inici, $longitud)
explode($sep, $s) // string → array
implode($sep, $arr) // array → string
sprintf("%.2f €", 9.9) // formata: "9.90 €"
htmlspecialchars($s) // escapa caràcters HTML (seguretat XSS)
nl2br($s) // \n → <br>
strip_tags($s) // elimina etiquetes HTMLNombres:
abs($n) / ceil($n) / floor($n) / round($n, $decimals)
sqrt($n) / pow($base, $exp)
min(...$nums) / max(...$nums)
rand($min, $max) // pseudoaleatori
random_int($min, $max) // criptogràficament segur
number_format($n, 2, ',', '.') // "1.234,56"Arrays: (vegeu secció anterior)
Data i hora:
date('Y-m-d H:i:s') // data/hora actual formatada
time() // timestamp Unix actual
strtotime('+1 month') // timestamp relatiu
$d = new DateTime();
$d->format('d/m/Y');
$d->add(new DateInterval('P1M')); // +1 mesAltres útils:
phpinfo() // informació de l'entorn PHP
var_dump($var) // tipus i valor (debug)
print_r($var) // valor llegible (debug)
isset($var) // comprova si la variable existeix i no és null
empty($var) // comprova si és buida/falsy
unset($var) // elimina la variable
is_int($v) / is_string($v) / is_array($v) / is_null($v)
intval($v) / floatval($v) / strval($v)
filter_var($v, FILTER_VALIDATE_EMAIL) // validació i sanejament
json_encode($arr) // array → JSON string
json_decode($json, true) // JSON string → array associativa<?php
/**
* Calcula el preu amb IVA.
*
* @param float $preu Preu sense IVA
* @param float $iva Percentatge d'IVA (0.21 per defecte)
* @return float Preu amb IVA
*/
function preuAmbIVA(float $preu, float $iva = 0.21): float {
return $preu * (1 + $iva);
}
echo preuAmbIVA(100); // 121.0
echo preuAmbIVA(100, 0.10); // 110.0
?>Paràmetres variables:
<?php
function suma(...$nombres): float {
return array_sum($nombres);
}
echo suma(1, 2, 3, 4, 5); // 15
?>Tipació estricta (recomanada):
<?php
declare(strict_types=1); // primera línia del fitxer
function divideix(int $a, int $b): float {
if ($b === 0) throw new DivisionByZeroError("Divisió per zero");
return $a / $b;
}
?>Named arguments (PHP 8+):
<?php
// array_fill(start_index, count, value)
array_fill(start_index: 0, count: 100, value: 50);
// Podem canviar l'ordre i saltar-nos paràmetres amb valor per defecte
?>Passar per referència:
<?php
function incrementa(int &$valor): void {
$valor++;
}
$x = 5;
incrementa($x);
echo $x; // 6
?>Funcions anònimes i funcions fletxa:
<?php
// Closure (funció anònima)
$doble = function($n) { return $n * 2; };
// Captura de variables externes
$factor = 3;
$multiplica = function($n) use ($factor) { return $n * $factor; };
// Funció fletxa (PHP 7.4+): captura automàtica de l'entorn
$multiplica = fn($n) => $n * $factor;
// Ús habitual en array_map, array_filter
$quadrats = array_map(fn($n) => $n ** 2, [1, 2, 3, 4]);
$parells = array_filter([1,2,3,4,5,6], fn($n) => $n % 2 === 0);
?>Variables variables:
<?php
$camp = 'titol';
$$camp = 'El meu títol';
echo $titol; // "El meu títol"
?>| Constant | Descripció |
|---|---|
E_ERROR |
Errors fatals que aturen l’execució |
E_WARNING |
Avisos que no aturen l’execució |
E_NOTICE |
Avisos lleugers (ús de variables no definides, etc.) |
E_PARSE |
Errors de sintaxi |
E_ALL |
Tots els errors i avisos |
<?php
// En fitxer php (sobreescriu php.ini temporalment)
error_reporting(E_ALL); // mostra tots els errors
ini_set('display_errors', '1'); // mostra errors per pantalla (NOMÉS dev)
ini_set('log_errors', '1'); // desa errors en un fitxer de log
ini_set('error_log', '/var/log/php-errors.log');
?>Mai activis display_errors en un
servidor de producció: podria revelar rutes, usuaris i contrasenyes.
<?php
function gestorErrors(int $errno, string $errstr, string $errfile, int $errline): bool {
$missatge = "[" . date('Y-m-d H:i:s') . "] Error $errno: $errstr a $errfile línia $errline";
error_log($missatge);
if ($errno === E_USER_ERROR) {
echo "<p>S'ha produït un error. Si us plau, contacteu l'administrador.</p>";
exit(1);
}
return true; // no executar el gestor intern de PHP
}
set_error_handler("gestorErrors");
// Generar errors personalitzats
trigger_error("Valor invàlid", E_USER_WARNING);
trigger_error("Error crític", E_USER_ERROR);
?><?php
declare(strict_types=1);
// Excepció personalitzada
class ValidacióException extends RuntimeException {
private array $errors;
public function __construct(array $errors) {
parent::__construct(implode(', ', $errors));
$this->errors = $errors;
}
public function getErrors(): array { return $this->errors; }
}
// Ús
function validaNom(string $nom): string {
if (empty($nom)) throw new ValidacióException(["El nom és obligatori"]);
if (strlen($nom) < 2) throw new ValidacióException(["El nom és massa curt"]);
return trim($nom);
}
try {
$nom = validaNom("");
echo "Nom vàlid: $nom";
} catch (ValidacióException $e) {
foreach ($e->getErrors() as $error) {
echo "- $error\n";
}
} catch (Exception $e) {
echo "Error inesperat: " . $e->getMessage();
} finally {
echo "Sempre s'executa";
}
?><?php
set_exception_handler(function(Throwable $e) {
error_log("[EXCEPCIÓ] " . $e->getMessage() . " a " . $e->getFile() . ":" . $e->getLine());
http_response_code(500);
echo "<h1>Error intern del servidor</h1>";
// En desenvolupament podries mostrar $e->getMessage()
});
?><!-- formulari.html -->
<form method="POST" action="processar.php">
<label>Nom: <input type="text" name="nom" required></label>
<label>Email: <input type="email" name="email"></label>
<label>Edat: <input type="number" name="edat" min="0" max="120"></label>
<label>
Gènere:
<select name="genere">
<option value="h">Home</option>
<option value="d">Dona</option>
<option value="a">Altres</option>
</select>
</label>
<label>
<input type="checkbox" name="accepta_condicions" value="1">
Accepto les condicions
</label>
<button type="submit">Enviar</button>
</form><?php
// processar.php
declare(strict_types=1);
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: formulari.html');
exit;
}
// 1. Recollir i netejar
$nom = trim(htmlspecialchars($_POST['nom'] ?? ''));
$email = trim($_POST['email'] ?? '');
$edat = (int) ($_POST['edat'] ?? 0);
$accepta = isset($_POST['accepta_condicions']);
// 2. Validar
$errors = [];
if (empty($nom)) {
$errors[] = "El nom és obligatori";
} elseif (mb_strlen($nom) < 2) {
$errors[] = "El nom ha de tenir almenys 2 caràcters";
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = "L'adreça de correu no és vàlida";
}
if ($edat < 0 || $edat > 120) {
$errors[] = "L'edat ha de ser entre 0 i 120";
}
if (!$accepta) {
$errors[] = "Cal acceptar les condicions";
}
// 3. Mostrar resultat
if (empty($errors)) {
echo "<p>Dades rebudes correctament: $nom, $email, $edat anys</p>";
// Aquí vindria la lògica: desar a BD, enviar email, etc.
} else {
echo "<ul class='errors'>";
foreach ($errors as $error) {
echo "<li>" . htmlspecialchars($error) . "</li>";
}
echo "</ul>";
}
?><?php
// Validació
filter_var($email, FILTER_VALIDATE_EMAIL); // email
filter_var($url, FILTER_VALIDATE_URL); // URL
filter_var($ip, FILTER_VALIDATE_IP); // IP
filter_var($n, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 120]]);
filter_var($n, FILTER_VALIDATE_FLOAT);
filter_var($b, FILTER_VALIDATE_BOOLEAN);
// Sanejament (elimina caràcters no desitjats)
filter_var($input, FILTER_SANITIZE_NUMBER_INT);
filter_var($input, FILTER_SANITIZE_EMAIL);
filter_var($input, FILTER_SANITIZE_URL);
// Equivalent manual per a HTML
htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
strip_tags($input);
trim($input);
?><form action="pujar.php" method="POST" enctype="multipart/form-data">
<input type="file" name="imatge" accept="image/*">
<button type="submit">Pujar</button>
</form><?php
// pujar.php
$tipus_permesos = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$mida_maxima = 2 * 1024 * 1024; // 2 MB
$directori_dest = __DIR__ . '/uploads/';
if (!isset($_FILES['imatge']) || $_FILES['imatge']['error'] !== UPLOAD_ERR_OK) {
die("Error en la pujada del fitxer");
}
$fitxer = $_FILES['imatge'];
// Verificar tipus MIME real (no el que diu el navegador)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$tipus_real = $finfo->file($fitxer['tmp_name']);
if (!in_array($tipus_real, $tipus_permesos)) {
die("Tipus de fitxer no permès: $tipus_real");
}
if ($fitxer['size'] > $mida_maxima) {
die("El fitxer és massa gran");
}
// Generar nom únic per evitar sobrescriure fitxers existents
$extensio = pathinfo($fitxer['name'], PATHINFO_EXTENSION);
$nom_segur = uniqid('img_', true) . '.' . strtolower($extensio);
$ruta_dest = $directori_dest . $nom_segur;
if (move_uploaded_file($fitxer['tmp_name'], $ruta_dest)) {
echo "Fitxer pujat correctament: $nom_segur";
} else {
die("No s'ha pogut moure el fitxer");
}
?><?php
// GENERAR HASH (en registrar l'usuari)
$clau_text_pla = $_POST['clau'];
$hash = password_hash($clau_text_pla, PASSWORD_DEFAULT);
// Desa $hash a la BD (camp VARCHAR(255))
// Exemple: $2y$10$6z7GKa9kpDN7KC3ICW1Hi.fdO/to7Y/...
// VERIFICAR (en fer login)
if (password_verify($clau_introduida, $hash_de_bd)) {
echo "Contrasenya correcta";
}
// Actualitzar l'algoritme si cal (cost computacional ha augmentat)
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
$hash_nou = password_hash($clau_text_pla, PASSWORD_DEFAULT);
// Actualitza el hash a la BD
}
?>
PASSWORD_DEFAULTusa bcrypt. PHP fa evolucionar l’algoritme amb el temps.
No usar SHA1, MD5 ni SHA256 directament: són massa ràpids i vulnerables a atacs de força bruta.
<?php
// login.php
declare(strict_types=1);
session_start();
// Si ja hi ha sessió, redirigir
if (isset($_SESSION['user_id'])) {
header('Location: dashboard.php');
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$usuari = trim($_POST['usuari'] ?? '');
$clau = $_POST['clau'] ?? '';
if (empty($usuari) || empty($clau)) {
$error = "Omple tots els camps";
} else {
try {
$pdo = new PDO('mysql:host=localhost;dbname=app;charset=utf8mb4', 'user', 'pass',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$stmt = $pdo->prepare("SELECT id, nom, clau_hash FROM usuaris WHERE email = ? LIMIT 1");
$stmt->execute([$usuari]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user && password_verify($clau, $user['clau_hash'])) {
// Login correcte
session_regenerate_id(true); // evita fixació de sessió
$_SESSION['user_id'] = $user['id'];
$_SESSION['nom'] = $user['nom'];
$_SESSION['rol'] = $user['rol'] ?? 'usuari';
$dest = $_GET['backto'] ?? 'dashboard.php';
header('Location: ' . $dest);
exit;
} else {
$error = "Credencials incorrectes";
// Registrar intent fallat (per a fail2ban o similar)
error_log("Login fallat per a l'usuari: $usuari des de " . $_SERVER['REMOTE_ADDR']);
}
} catch (PDOException $e) {
error_log("Error BD en login: " . $e->getMessage());
$error = "Error intern. Torna-ho a provar.";
}
}
}
?>
<!DOCTYPE html>
<html lang="ca">
<body>
<?php if ($error): ?>
<p class="error"><?= htmlspecialchars($error) ?></p>
<?php endif; ?>
<form method="POST">
<input type="email" name="usuari" placeholder="Email" required>
<input type="password" name="clau" placeholder="Contrasenya" required>
<button type="submit">Entrar</button>
</form>
</body>
</html><?php
// auth.php — incloure a l'inici de cada pàgina protegida
declare(strict_types=1);
function requereixLogin(string $redirigir_a = 'login.php'): void {
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user_id'])) {
$backto = urlencode($_SERVER['REQUEST_URI']);
header("Location: $redirigir_a?backto=$backto");
exit;
}
}
function requereixRol(string ...$rols_permesos): void {
requereixLogin();
$rol_usuari = $_SESSION['rol'] ?? 'usuari';
if (!in_array($rol_usuari, $rols_permesos, true)) {
http_response_code(403);
echo "<h1>403 — Accés denegat</h1>";
echo "<p>No tens permís per accedir a aquesta pàgina.</p>";
exit;
}
}
?><?php
// dashboard.php — pàgina accessible per a qualsevol usuari autenticat
require_once 'auth.php';
requereixLogin();
echo "Benvingut, " . htmlspecialchars($_SESSION['nom']);
?>
<?php
// admin.php — pàgina accessible només per a administradors
require_once 'auth.php';
requereixRol('admin');
echo "Panell d'administrador";
?>
<?php
// editor.php — pàgina accessible per a editors i administradors
require_once 'auth.php';
requereixRol('editor', 'admin');
?><?php
// Definir permisos per rol
const PERMISOS = [
'usuari' => ['llegir'],
'editor' => ['llegir', 'escriure', 'editar'],
'admin' => ['llegir', 'escriure', 'editar', 'esborrar', 'gestionar_usuaris'],
'superadmin'=> ['*'], // tots els permisos
];
function tePermis(string $permis): bool {
$rol = $_SESSION['rol'] ?? 'usuari';
$permisos_rol = PERMISOS[$rol] ?? [];
return in_array('*', $permisos_rol) || in_array($permis, $permisos_rol);
}
// Ús a les vistes
if (tePermis('esborrar')): ?>
<button class="btn-danger">Esborrar article</button>
<?php endif; ?><?php
// Generar token CSRF
function generaTokenCSRF(): string {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
// Validar token CSRF
function validaTokenCSRF(): bool {
$token_rebut = $_POST['csrf_token'] ?? '';
$token_sessio = $_SESSION['csrf_token'] ?? '';
return hash_equals($token_sessio, $token_rebut);
}
?>
<!-- Al formulari -->
<form method="POST" action="processar.php">
<input type="hidden" name="csrf_token" value="<?= generaTokenCSRF() ?>">
<!-- resta del formulari -->
</form>
<?php
// En processar
if (!validaTokenCSRF()) {
http_response_code(403);
die("Token CSRF invàlid");
}
?><?php
// 1. Iniciar (a l'inici de CADA pàgina que usa sessions, abans de qualsevol sortida)
session_start();
// 2. Emmagatzemar dades
$_SESSION['user_id'] = 42;
$_SESSION['nom'] = 'Joan';
$_SESSION['rol'] = 'admin';
$_SESSION['darrer_accés'] = time();
// 3. Llegir dades
echo $_SESSION['nom']; // Joan
echo isset($_SESSION['nom']) ? 'sí' : 'no';
// 4. Eliminar una variable de sessió
unset($_SESSION['darrer_accés']);
// 5. Tancar la sessió completament (logout)
$_SESSION = []; // buidar totes les variables
if (ini_get("session.use_cookies")) { // destruir la cookie de sessió
$params = session_get_cookie_params();
setcookie(
session_name(), '', time() - 42000,
$params['path'], $params['domain'],
$params['secure'], $params['httponly']
);
}
session_destroy();
?><?php
// Modificar opcions en cridar session_start()
session_start([
'cookie_lifetime' => 3600, // la cookie expira en 1 hora
'cookie_secure' => true, // HTTPS obligatori
'cookie_httponly' => true, // inaccessible per JavaScript
'cookie_samesite' => 'Strict', // prevenció CSRF
'gc_maxlifetime' => 3600, // dades de sessió al servidor: 1 hora
]);
// Regenerar ID per evitar fixació de sessió (fer-ho en el login)
session_regenerate_id(true);
?><?php
// pàgina_personal.php
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
// Cada usuari veu NOMÉS les seves dades
$user_id = (int) $_SESSION['user_id']; // cast a int per seguretat
$pdo = connexioBD();
$stmt = $pdo->prepare("SELECT * FROM comandes WHERE usuari_id = ?");
$stmt->execute([$user_id]);
$comandes = $stmt->fetchAll();
// Cada usuari veu sols les seves comandes
// L'aïllament el garanteix la sessió + la consulta SQL amb WHERE usuari_id = ?
?>php.iniEl fitxer principal de configuració de PHP. A Ubuntu 26.04 amb Apache, la ubicació és:
| Sistema | Ubicació |
|---|---|
| Ubuntu 26.04 + Apache | /etc/php/8.x/apache2/php.ini |
| Ubuntu 26.04 (CLI) | /etc/php/8.x/cli/php.ini |
Per saber la versió exacta de PHP i el fitxer que s’usa:
php -v # mostra la versió (ex. 8.3.x)
php --ini # mostra el php.ini que usa la CLI
apache2ctl -t -D DUMP_MODULES # comprova que el mòdul PHP és actiuOpcions clau:
; ===== ERRORS (DEV) =====
display_errors = On
error_reporting = E_ALL
log_errors = On
error_log = /var/log/php_errors.log
; ===== ERRORS (PRODUCCIÓ) =====
display_errors = Off
display_startup_errors = Off
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
log_errors = On
error_log = /var/log/php_errors.log
; ===== FITXERS PUJATS (recomanat per a WordPress) =====
file_uploads = On
upload_max_filesize = 64M
post_max_size = 68M ; ha de ser > upload_max_filesize
max_file_uploads = 20
; ===== RENDIMENT =====
memory_limit = 256M ; WordPress recomana 256M
max_execution_time = 60
max_input_time = 60
max_input_vars = 3000 ; necessari per a temes/plugins complexos
; ===== EXTENSIONS (ja actives si s'ha seguit la instal·lació) =====
; Les extensions s'activen com a paquets APT a Ubuntu,
; no editant php.ini directament. Comprova-les amb phpinfo().
; ===== SESSIONS =====
session.cookie_httponly = 1
session.cookie_secure = 1 ; requereix HTTPS
session.gc_maxlifetime = 1440
session.use_strict_mode = 1
; ===== OPCACHE (rendiment) =====
opcache.enable = 1
opcache.memory_consumption = 128
opcache.max_accelerated_files = 4000Després de modificar
php.ini, cal reiniciar Apache:sudo systemctl restart apache2
<?php
ini_set('display_errors', '1');
ini_set('memory_limit', '256M');
ini_set('max_execution_time', '120');
echo ini_get('memory_limit'); // 256M
?>| SGBD | Característiques | Usos habituals |
|---|---|---|
| MySQL / MariaDB | El més usat en web, ràpid, codi obert | WordPress, LAMP |
| PostgreSQL | Funcionalitats avançades, ACID complet | Aplicacions empresarials, geolocalització |
| SQLite | Fitxer únic, sense servidor | Apps mòbils, prototips, tests |
| MongoDB | NoSQL orientat a documents | APIs, big data, dades no estructurades |
| Redis | Clau-valor en memòria | Caching, sessions, cues de missatges |
PHP ofereix dues API per connectar-se a MySQL:
| PDO | MySQLi | |
|---|---|---|
| Portabilitat | Suporta 12 SGBD | Només MySQL/MariaDB |
| Prepared statements | Sí (nomenats i posicionals) | Sí (només posicionals) |
| Estil | OO | OO i procedural |
| Recomanació | Preferida | Per a funcionalitats MySQL específiques |
En aquest document usem PDO per portabilitat.
A Ubuntu 26.04, les extensions de PHP s’instal·len com a paquets APT,
no editant php.ini directament:
# Ubuntu 26.04 — instal·lar extensions per a MariaDB/MySQL
sudo apt install php-mysql php-mbstring php-xml php-curl \
php-zip php-gd php-intl php-bcmath -y
sudo systemctl restart apache2Per verificar que les extensions estan actives:
php -m | grep -E "pdo_mysql|mysqli|mbstring|gd|curl"O bé consultant phpinfo() al navegador.
<?php
declare(strict_types=1);
function connexioBD(): PDO {
$dsn = 'mysql:host=localhost;dbname=la_meva_bd;charset=utf8mb4';
$usuari = 'nom_usuari';
$clau = 'contrasenya';
$opcions = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // llença excepcions
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // arrays associatives
PDO::ATTR_EMULATE_PREPARES => false, // prepared statements reals
];
try {
return new PDO($dsn, $usuari, $clau, $opcions);
} catch (PDOException $e) {
// No mostrar detalls de connexió per pantalla en producció
error_log("Error de connexió BD: " . $e->getMessage());
throw new RuntimeException("No s'ha pogut connectar a la base de dades");
}
}
// Ús
$pdo = connexioBD();<?php
// config/database.php
return [
'host' => 'localhost',
'dbname' => 'la_meva_bd',
'charset' => 'utf8mb4',
'username' => 'nom_usuari',
'password' => 'contrasenya',
];<?php
// Carregar configuració
$config = require __DIR__ . '/config/database.php';
$dsn = "mysql:host={$config['host']};dbname={$config['dbname']};charset={$config['charset']}";
$pdo = new PDO($dsn, $config['username'], $config['password']);<?php
$pdo = null; // allibera la connexió
?><?php
// Connexió sense especificar dbname per poder crear la BD
$pdo = new PDO('mysql:host=localhost;charset=utf8mb4', 'root', 'rootpass',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$pdo->exec("CREATE DATABASE IF NOT EXISTS botiga
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci");
echo "Base de dades creada";
// Seleccionar-la
$pdo->exec("USE botiga");
?><?php
$pdo = connexioBD();
// Taula d'usuaris
$pdo->exec("
CREATE TABLE IF NOT EXISTS usuaris (
id INT AUTO_INCREMENT PRIMARY KEY,
nom VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
clau_hash VARCHAR(255) NOT NULL,
rol ENUM('usuari','editor','admin') DEFAULT 'usuari',
actiu TINYINT(1) DEFAULT 1,
creat_el TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
modificat_el TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
// Taula de productes
$pdo->exec("
CREATE TABLE IF NOT EXISTS productes (
id INT AUTO_INCREMENT PRIMARY KEY,
nom VARCHAR(200) NOT NULL,
descripcio TEXT,
preu DECIMAL(10,2) NOT NULL,
estoc INT DEFAULT 0,
actiu TINYINT(1) DEFAULT 1,
creat_el TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
// Taula de comandes (clau forana)
$pdo->exec("
CREATE TABLE IF NOT EXISTS comandes (
id INT AUTO_INCREMENT PRIMARY KEY,
usuari_id INT NOT NULL,
total DECIMAL(10,2) NOT NULL,
estat ENUM('pendent','pagada','enviada','cancel·lada') DEFAULT 'pendent',
creat_el TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (usuari_id) REFERENCES usuaris(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
echo "Taules creades correctament";
?><?php
$pdo = connexioBD();
// Tots els productes actius
$stmt = $pdo->query("SELECT * FROM productes WHERE actiu = 1 ORDER BY nom");
$productes = $stmt->fetchAll(); // array de tots els registres
foreach ($productes as $p) {
echo "{$p['nom']}: {$p['preu']} €<br>";
}
// Nombre de files
echo "Total: " . $stmt->rowCount();
?><?php
$pdo = connexioBD();
// Paràmetres nomenats (:nom)
$sql = "SELECT * FROM productes
WHERE preu < :preu_max AND actiu = 1
ORDER BY preu ASC
LIMIT :limit";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':preu_max', 50.0, PDO::PARAM_STR); // float usa PARAM_STR
$stmt->bindValue(':limit', 10, PDO::PARAM_INT);
$stmt->execute();
$productes = $stmt->fetchAll();
// O amb execute() directament:
$stmt = $pdo->prepare("SELECT * FROM productes WHERE preu < ? AND actiu = 1");
$stmt->execute([50.0]);
$productes = $stmt->fetchAll();
?><?php
$pdo = connexioBD();
$stmt = $pdo->prepare("SELECT * FROM usuaris WHERE id = ?");
$stmt->execute([$id]);
$usuari = $stmt->fetch(); // un sol registre o false si no existeix
if ($usuari) {
echo "Nom: {$usuari['nom']}";
} else {
echo "Usuari no trobat";
}
// Obtenir un sol valor escalar
$stmt = $pdo->prepare("SELECT COUNT(*) FROM productes WHERE actiu = 1");
$stmt->execute();
$total = $stmt->fetchColumn();
echo "Total actius: $total";
?><?php
$sql = "
SELECT c.id, c.total, c.estat, u.nom AS nom_usuari, u.email
FROM comandes c
JOIN usuaris u ON c.usuari_id = u.id
WHERE c.estat = ?
ORDER BY c.creat_el DESC
";
$stmt = $pdo->prepare($sql);
$stmt->execute(['pendent']);
$comandes = $stmt->fetchAll();
?><?php
$pdo = connexioBD();
// Inserció simple
$stmt = $pdo->prepare("
INSERT INTO productes (nom, descripcio, preu, estoc)
VALUES (:nom, :descripcio, :preu, :estoc)
");
$stmt->execute([
':nom' => 'Teclat mecànic',
':descripcio' => 'Teclat mecànic retroil·luminat',
':preu' => 79.99,
':estoc' => 50,
]);
$id_nou = (int) $pdo->lastInsertId();
echo "Producte inserit amb ID: $id_nou";
// Inserció múltiple (transacció per eficiència)
$productes = [
['Ratolí ergonòmic', 29.99, 100],
['Monitor 24"', 199.00, 20],
['Auriculars BT', 49.99, 75],
];
$stmt = $pdo->prepare("INSERT INTO productes (nom, preu, estoc) VALUES (?, ?, ?)");
$pdo->beginTransaction();
try {
foreach ($productes as $p) {
$stmt->execute($p);
}
$pdo->commit();
echo "Inserció múltiple completada";
} catch (PDOException $e) {
$pdo->rollBack();
error_log("Error en inserció múltiple: " . $e->getMessage());
throw $e;
}
?><?php
$pdo = connexioBD();
// Actualitzar un registre
$stmt = $pdo->prepare("
UPDATE productes
SET preu = :preu, estoc = :estoc, modificat_el = NOW()
WHERE id = :id AND actiu = 1
");
$stmt->execute([
':preu' => 89.99,
':estoc' => 45,
':id' => 1,
]);
echo "Files actualitzades: " . $stmt->rowCount();
// Actualitzar múltiples registres
$stmt = $pdo->prepare("UPDATE productes SET actiu = 0 WHERE estoc = 0");
$stmt->execute();
echo "Productes desactivats: " . $stmt->rowCount();
?><?php
$pdo = connexioBD();
// Esborrat lògic (recomanat: no elimina el registre)
$stmt = $pdo->prepare("UPDATE productes SET actiu = 0 WHERE id = ?");
$stmt->execute([$id_producte]);
// Esborrat físic
$stmt = $pdo->prepare("DELETE FROM productes WHERE id = ?");
$stmt->execute([$id_producte]);
echo "Files esborrades: " . $stmt->rowCount();
?><?php
$pdo = connexioBD();
try {
$pdo->beginTransaction();
// Operació 1: crear comanda
$stmt = $pdo->prepare("INSERT INTO comandes (usuari_id, total) VALUES (?, ?)");
$stmt->execute([$usuari_id, $total]);
$comanda_id = (int) $pdo->lastInsertId();
// Operació 2: reduir estoc
$stmt = $pdo->prepare("UPDATE productes SET estoc = estoc - ? WHERE id = ? AND estoc >= ?");
$stmt->execute([$quantitat, $producte_id, $quantitat]);
if ($stmt->rowCount() === 0) {
throw new RuntimeException("Estoc insuficient");
}
$pdo->commit();
echo "Comanda $comanda_id creada correctament";
} catch (Exception $e) {
$pdo->rollBack();
error_log("Error en transacció: " . $e->getMessage());
echo "Error: " . $e->getMessage();
}
?><?php
declare(strict_types=1);
/**
* Valida i neteja les dades d'un producte.
* @throws InvalidArgumentException si les dades no són vàlides
*/
function validaProducte(array $dades): array {
$errors = [];
$nom = trim($dades['nom'] ?? '');
if (empty($nom)) {
$errors[] = "El nom és obligatori";
} elseif (mb_strlen($nom) > 200) {
$errors[] = "El nom no pot superar 200 caràcters";
}
$preu = filter_var($dades['preu'] ?? '', FILTER_VALIDATE_FLOAT);
if ($preu === false || $preu < 0) {
$errors[] = "El preu ha de ser un número positiu";
}
$estoc = filter_var($dades['estoc'] ?? '', FILTER_VALIDATE_INT,
['options' => ['min_range' => 0]]);
if ($estoc === false) {
$errors[] = "L'estoc ha de ser un enter no negatiu";
}
if (!empty($errors)) {
throw new InvalidArgumentException(implode('; ', $errors));
}
return [
'nom' => htmlspecialchars($nom, ENT_QUOTES, 'UTF-8'),
'preu' => (float) $preu,
'estoc' => (int) $estoc,
];
}
// Ús
try {
$dades_validades = validaProducte($_POST);
// Inserir a la BD...
} catch (InvalidArgumentException $e) {
echo "Error de validació: " . $e->getMessage();
}
?><?php
function existeixEmail(PDO $pdo, string $email, ?int $excloure_id = null): bool {
$sql = "SELECT COUNT(*) FROM usuaris WHERE email = ?";
$params = [$email];
if ($excloure_id !== null) {
$sql .= " AND id != ?";
$params[] = $excloure_id;
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return (int) $stmt->fetchColumn() > 0;
}
if (existeixEmail($pdo, $_POST['email'])) {
$errors[] = "Ja existeix un compte amb aquest email";
}
?><?php
$opcions = [
// ERRMODE_SILENT (defecte): errors silenciosos, cal comprovar manualment
PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT,
// ERRMODE_WARNING: genera un E_WARNING (no atura l'execució)
PDO::ATTR_ERRMODE => PDO::ERRMODE_WARNING,
// ERRMODE_EXCEPTION (recomanat): llença PDOException
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
];
?><?php
function insereixProducte(PDO $pdo, array $dades): int {
try {
$stmt = $pdo->prepare("
INSERT INTO productes (nom, preu, estoc)
VALUES (:nom, :preu, :estoc)
");
$stmt->execute($dades);
return (int) $pdo->lastInsertId();
} catch (PDOException $e) {
$codi = (int) $e->getCode();
// Error de clau duplicada (SQLSTATE 23000)
if ($codi === 23000) {
throw new RuntimeException("Ja existeix un producte amb aquest nom");
}
// Altres errors: registrar i relllençar
error_log("Error BD insereixProducte: [{$e->getCode()}] {$e->getMessage()}");
throw new RuntimeException("Error en desar el producte. Torna-ho a provar.");
}
}
?><?php
// VULNERABLE — MAI fer-ho així
$id = $_GET['id'];
$sql = "SELECT * FROM usuaris WHERE id = $id";
// Si id = "1 OR 1=1", retorna tots els usuaris
// VULNERABLE — escapat manual insuficient
$id = mysql_real_escape_string($_GET['id']); // funció eliminada a PHP 7
// CORRECTE — prepared statements
$stmt = $pdo->prepare("SELECT * FROM usuaris WHERE id = ?");
$stmt->execute([(int) $_GET['id']]);
$usuari = $stmt->fetch();
?>-- Crear un usuari de BD amb NOMÉS els permisos necessaris
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'contrasenya_segura';
-- Permisos mínims per a l'aplicació web (no DROP, no CREATE, no GRANT)
GRANT SELECT, INSERT, UPDATE, DELETE ON botiga.* TO 'app_user'@'localhost';
FLUSH PRIVILEGES;<?php
// MAL: credencials al codi
$pdo = new PDO('mysql:host=localhost;dbname=bd', 'root', 'password123');
// BÉ: credencials en un fitxer fora de l'arrel web
// Estructura del projecte:
// /var/www/html/ ← arrel web (accessible)
// index.php
// config/ ← cal protegir amb .htaccess o moure fora de l'arrel
// /etc/app/ ← fora de l'arrel web (no accessible)
// database.php
// database.php (fora de l'arrel web)
return [
'host' => getenv('DB_HOST') ?: 'localhost',
'name' => getenv('DB_NAME') ?: 'botiga',
'user' => getenv('DB_USER') ?: 'app_user',
'pass' => getenv('DB_PASS') ?: '',
];
?># .htaccess per protegir el directori config/
<Directory /var/www/html/config>
Require all denied
</Directory><?php
// Verificar la connexió
try {
$pdo = connexioBD();
$stmt = $pdo->query("SELECT 1");
echo "Connexió a la BD: OK";
} catch (Exception $e) {
echo "Connexió a la BD: FALLIDA — " . $e->getMessage();
}
// Informació del servidor MySQL
$stmt = $pdo->query("SELECT VERSION() AS versio, NOW() AS ara");
$info = $stmt->fetch();
echo "MySQL versió: {$info['versio']}, Hora servidor: {$info['ara']}";
?><?php
$inici = microtime(true);
// Consulta a mesurar
$stmt = $pdo->query("SELECT * FROM productes WHERE actiu = 1");
$productes = $stmt->fetchAll();
$temps = (microtime(true) - $inici) * 1000;
echo sprintf("Consulta: %.2f ms, %d registres", $temps, count($productes));
?><?php
$sql = "SELECT * FROM productes WHERE nom LIKE '%teclat%'";
$stmt = $pdo->query("EXPLAIN $sql");
$pla = $stmt->fetchAll();
print_r($pla);
// Mostra si s'usen índexs i el cost estimat de la consulta
?><?php
$pagina = max(1, (int) ($_GET['pagina'] ?? 1));
$resultats_pagina = 20;
$offset = ($pagina - 1) * $resultats_pagina;
// Comptar total
$stmt = $pdo->query("SELECT COUNT(*) FROM productes WHERE actiu = 1");
$total = (int) $stmt->fetchColumn();
$total_pagines = (int) ceil($total / $resultats_pagina);
// Obtenir la pàgina actual
$stmt = $pdo->prepare("SELECT * FROM productes WHERE actiu = 1 LIMIT ? OFFSET ?");
$stmt->bindValue(1, $resultats_pagina, PDO::PARAM_INT);
$stmt->bindValue(2, $offset, PDO::PARAM_INT);
$stmt->execute();
$productes = $stmt->fetchAll();
?>
<p>Pàgina <?= $pagina ?> de <?= $total_pagines ?> (<?= $total ?> resultats)</p><?php
// 1. Usar índexs a les columnes que s'usen al WHERE, JOIN i ORDER BY
// (configuració a MySQL, no a PHP)
// 2. Seleccionar SOLS les columnes necessàries
$stmt = $pdo->query("SELECT id, nom, preu FROM productes"); // no SELECT *
// 3. Reutilitzar prepared statements en bucles
$stmt = $pdo->prepare("INSERT INTO log (event, dades) VALUES (?, ?)");
foreach ($events as $e) {
$stmt->execute([$e['tipus'], json_encode($e['dades'])]);
}
// 4. Usar transaccions per a insercions múltiples (molt més ràpid)
$pdo->beginTransaction();
// ... múltiples inserts ...
$pdo->commit();
// 5. Limitar sempre els resultats
$stmt = $pdo->prepare("SELECT * FROM logs ORDER BY creat_el DESC LIMIT 100");
?>Un gestor de continguts (Content Management System, CMS) és una aplicació web que permet crear, gestionar i publicar contingut digital sense necessitat de programar cada vegada. Separa el contingut de la presentació i ofereix una interfície d’administració gràfica.
| CMS | Quota mercat | Tecnologia | Usos típics |
|---|---|---|---|
| WordPress | ~43% web mundial | PHP + MySQL | Blogs, webs corporatives, e-commerce |
| Drupal | ~2% | PHP + MySQL | Portals complexos, grans institucions |
| Joomla | ~2% | PHP + MySQL | Webs de tot mena |
| Magento/Adobe Commerce | — | PHP + MySQL | E-commerce |
| Shopify | — | Ruby | E-commerce (SaaS) |
| Ghost | — | Node.js | Blogs professionals |
Dades de desembre de 2025 Sketchweb Microblog
WordPress és el CMS dominant i el que estudiem en detall per la seva penetració i arquitectura representativa.
/var/www/html/wordpress/
├── wp-admin/ ← Panell d'administació (no modificar)
├── wp-includes/ ← Nucli de WordPress (no modificar)
└── wp-content/ ← Tot el contingut personalitzable
├── themes/ ← Temes (aparença)
│ ├── twentytwentyseven/ ← Tema per defecte de WordPress 7
│ └── el-meu-tema/ ← Tema personalitzat
├── plugins/ ← Plugins (funcionalitats)
│ ├── woocommerce/
│ └── el-meu-plugin/ ← Plugin personalitzat
├── uploads/ ← Fitxers pujats pels usuaris
└── languages/ ← Traduccions// wp-config.php — configuració principal (a l'arrel)
define('DB_NAME', 'nom_base_dades');
define('DB_USER', 'usuari_bd');
define('DB_PASSWORD', 'contrasenya_bd');
define('DB_HOST', 'localhost');
define('DB_CHARSET', 'utf8mb4');
// Claus de seguretat (generar a https://api.wordpress.org/secret-key/1.1/salt/)
define('AUTH_KEY', 'posar clau única aquí');
// ...
// Mode de debug (activar en desenvolupament)
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true); // desa errors a wp-content/debug.log
define('WP_DEBUG_DISPLAY', false); // no mostrar per pantalla en produccióWordPress gestiona el contingut com a posts de diferent tipus:
| Post Type | Descripció |
|---|---|
post |
Entrades del blog |
page |
Pàgines estàtiques |
attachment |
Fitxers multimèdia |
custom |
Qualsevol tipus personalitzat |
WordPress segueix una jerarquia per escollir quina plantilla renderitza cada URL:
Pàgina concreta → page-{slug}.php → page-{id}.php → page.php
Entrada blog → single-{type}.php → single.php → singular.php
Arxiu → archive-{type}.php → archive.php
Cerca → search.php
Error 404 → 404.php
Cap dels anteriors → index.php (obligatori)wp-content/themes/el-meu-tema/
├── style.css ← obligatori (capçalera amb metadades)
├── index.php ← obligatori (plantilla principal)
├── functions.php ← funcions del tema i hooks
├── header.php ← capçalera reutilitzable
├── footer.php ← peu reutilitzable
├── sidebar.php ← barra lateral reutilitzable
├── single.php ← plantilla per a entrades
├── page.php ← plantilla per a pàgines
├── archive.php ← plantilla per a arxius
├── 404.php ← pàgina d'error
└── screenshot.png ← previsualització al panell (1200×900 px)style.css —
capçalera obligatòria/*
Theme Name: El Meu Tema
Theme URI: https://exemple.cat/el-meu-tema
Description: Tema personalitzat per al mòdul 0376.
Author: Ramon López
Author URI: https://proferamon.com
Version: 1.0.0
License: GNU General Public License v2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: el-meu-tema
*/
/* Aquí els estils CSS del tema */
body {
font-family: 'Segoe UI', sans-serif;
line-height: 1.6;
color: #333;
}functions.php —
registrar funcionalitats<?php
// Activar funcionalitats del tema
function el_meu_tema_setup(): void {
// Suport per a imatge destacada
add_theme_support('post-thumbnails');
// Suport per a títol dinàmic a <title>
add_theme_support('title-tag');
// Suport per a HTML5
add_theme_support('html5', ['search-form', 'comment-form', 'gallery']);
// Registrar menús de navegació
register_nav_menus([
'primary' => __('Menú principal', 'el-meu-tema'),
'footer' => __('Menú del peu', 'el-meu-tema'),
]);
}
add_action('after_setup_theme', 'el_meu_tema_setup');
// Encolar CSS i JS (mai incloure'ls directament amb <link> o <script>)
function el_meu_tema_scripts(): void {
wp_enqueue_style(
'el-meu-tema-style',
get_stylesheet_uri(),
[],
'1.0.0'
);
wp_enqueue_style(
'el-meu-tema-fonts',
'https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700',
[],
null
);
wp_enqueue_script(
'el-meu-tema-script',
get_template_directory_uri() . '/js/main.js',
['jquery'], // dependències
'1.0.0',
true // carregar al footer
);
// Passar variables PHP a JavaScript
wp_localize_script('el-meu-tema-script', 'temaVars', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('el-meu-tema-nonce'),
]);
}
add_action('wp_enqueue_scripts', 'el_meu_tema_scripts');
// Registrar zona de widgets
function el_meu_tema_widgets(): void {
register_sidebar([
'name' => __('Barra lateral', 'el-meu-tema'),
'id' => 'sidebar-1',
'before_widget' => '<section class="widget">',
'after_widget' => '</section>',
'before_title' => '<h2 class="widget-title">',
'after_title' => '</h2>',
]);
}
add_action('widgets_init', 'el_meu_tema_widgets');
?>header.php — capçalera<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<?php wp_head(); // OBLIGATORI: encola CSS, JS i metadades ?>
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>
<header class="site-header">
<div class="container">
<a href="<?php echo esc_url(home_url('/')); ?>" class="site-title">
<?php bloginfo('name'); ?>
</a>
<?php
wp_nav_menu([
'theme_location' => 'primary',
'menu_class' => 'nav-menu',
'container' => 'nav',
]);
?>
</div>
</header>footer.php — peu<footer class="site-footer">
<div class="container">
<p>© <?php echo date('Y'); ?> <?php bloginfo('name'); ?></p>
</div>
</footer>
<?php wp_footer(); // OBLIGATORI: encola JavaScript del peu ?>
</body>
</html>index.php
— plantilla principal (Loop de WordPress)<?php get_header(); ?>
<main class="site-main">
<div class="container">
<?php if (have_posts()): ?>
<?php while (have_posts()): the_post(); ?>
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
<header>
<h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
<p>
<time datetime="<?php echo get_the_date('c'); ?>">
<?php echo get_the_date(); ?>
</time>
per <?php the_author(); ?>
</p>
</header>
<?php if (has_post_thumbnail()): ?>
<a href="<?php the_permalink(); ?>">
<?php the_post_thumbnail('medium'); ?>
</a>
<?php endif; ?>
<div class="entry-content">
<?php the_excerpt(); ?>
</div>
<a href="<?php the_permalink(); ?>">Llegir més →</a>
</article>
<?php endwhile; ?>
<?php the_posts_navigation(); ?>
<?php else: ?>
<p>No s'ha trobat contingut.</p>
<?php endif; ?>
</div>
</main>
<?php get_sidebar(); ?>
<?php get_footer(); ?>Per modificar un tema existent sense perdre els canvis en actualitzar:
wp-content/themes/
├── twentytwentyseven/ ← Tema pare per defecte de WordPress 7 (no tocar)
└── twentytwentyseven-fill/ ← Tema fill (les nostres modificacions)
├── style.css
└── functions.php/* style.css del tema fill */
/*
Theme Name: Twenty Twenty-Seven Fill
Template: twentytwentyseven
Version: 1.0.0
*/
/* Sobreescriure estils del pare */
.entry-title { color: #005f73; }<?php
// functions.php del tema fill
function fill_enqueue_parent_styles(): void {
wp_enqueue_style(
'parent-style',
get_template_directory_uri() . '/style.css'
);
}
add_action('wp_enqueue_scripts', 'fill_enqueue_parent_styles');
?>wp-content/plugins/el-meu-plugin/
├── el-meu-plugin.php ← fitxer principal (obligatori, mateix nom que la carpeta)
├── readme.txt ← documentació (format estàndard WP)
├── includes/
│ ├── class-plugin.php ← classe principal
│ └── helpers.php ← funcions auxiliars
└── assets/
├── css/
└── js/el-meu-plugin.php
— capçalera obligatòria<?php
/**
* Plugin Name: El Meu Plugin
* Plugin URI: https://exemple.cat/el-meu-plugin
* Description: Plugin d'exemple per al mòdul 0376.
* Version: 1.0.0
* Requires at least: 7.0
* Requires PHP: 8.2
* Author: Ramon López
* Author URI: https://proferamon.com
* License: GPL v2 or later
* Text Domain: el-meu-plugin
*/
// Evitar accés directe al fitxer
if (!defined('ABSPATH')) {
exit;
}
// Constants del plugin
define('EMP_VERSIO', '1.0.0');
define('EMP_DIR', plugin_dir_path(__FILE__));
define('EMP_URL', plugin_dir_url(__FILE__));
// Carregar fitxers
require_once EMP_DIR . 'includes/class-plugin.php';
// Inicialitzar el plugin
add_action('plugins_loaded', function() {
ElMeuPlugin::getInstance();
});
?>WordPress usa un sistema de hooks per permetre que plugins i temes s’enganxin al cicle d’execució:
<?php
// ACTION: s'executa en un punt concret (sense valor de retorn)
// add_action($nom_hook, $funció, $prioritat, $num_args)
add_action('wp_head', function() {
echo '<meta name="generator" content="El meu plugin 1.0">';
});
add_action('save_post', function(int $post_id, WP_Post $post) {
if ($post->post_type !== 'post') return;
error_log("Post desat: {$post->post_title}");
}, 10, 2); // prioritat 10, accepta 2 arguments
// FILTER: intercepta i modifica un valor
// add_filter($nom_hook, $funció, $prioritat, $num_args)
add_filter('the_title', function(string $titol): string {
return strtoupper($titol); // tots els títols en majúscules
});
add_filter('the_content', function(string $contingut): string {
if (!is_single()) return $contingut;
return $contingut . '<div class="signatura">Escrit per ' . get_the_author() . '</div>';
});
?><?php
add_action('init', function() {
register_post_type('producte', [
'labels' => [
'name' => 'Productes',
'singular_name' => 'Producte',
'add_new_item' => 'Afegir producte',
'edit_item' => 'Editar producte',
],
'public' => true,
'has_archive' => true,
'menu_icon' => 'dashicons-cart',
'supports' => ['title', 'editor', 'thumbnail', 'custom-fields'],
'rewrite' => ['slug' => 'productes'],
'show_in_rest'=> true, // necessari per a l'editor Gutenberg
]);
});
?><?php
add_action('init', function() {
register_taxonomy('categoria_producte', 'producte', [
'labels' => [
'name' => 'Categories de producte',
'singular_name' => 'Categoria de producte',
],
'hierarchical' => true, // com les categories (false = etiquetes)
'show_in_rest' => true,
'rewrite' => ['slug' => 'categoria-producte'],
]);
});
?><?php
// Registrar la pàgina de configuració
add_action('admin_menu', function() {
add_options_page(
'Configuració El Meu Plugin',
'El Meu Plugin',
'manage_options', // capacitat requerida
'el-meu-plugin', // slug
'emp_renderitza_opcions'
);
});
// Registrar les opcions
add_action('admin_init', function() {
register_setting('emp_opcions_grup', 'emp_color_accent');
register_setting('emp_opcions_grup', 'emp_mostrar_data',
['sanitize_callback' => 'absint']); // sanititza
});
// Renderitzar la pàgina
function emp_renderitza_opcions(): void {
if (!current_user_can('manage_options')) {
wp_die(__('No tens permís per accedir a aquesta pàgina.'));
}
?>
<div class="wrap">
<h1>Configuració El Meu Plugin</h1>
<form method="POST" action="options.php">
<?php
settings_fields('emp_opcions_grup');
do_settings_sections('el-meu-plugin');
?>
<table class="form-table">
<tr>
<th>Color accent</th>
<td>
<input type="color" name="emp_color_accent"
value="<?= esc_attr(get_option('emp_color_accent', '#005f73')) ?>">
</td>
</tr>
<tr>
<th>Mostrar data</th>
<td>
<input type="checkbox" name="emp_mostrar_data" value="1"
<?php checked(1, get_option('emp_mostrar_data'), true); ?>>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
</div>
<?php
}
?><?php
// Registrar shortcode [contacte_caixa titol="Escriu-nos"]
add_shortcode('contacte_caixa', function(array $atribs, string $contingut = ''): string {
$atribs = shortcode_atts([
'titol' => 'Contacta\'ns',
'color' => '#005f73',
], $atribs, 'contacte_caixa');
ob_start();
?>
<div class="caixa-contacte" style="border-color: <?= esc_attr($atribs['color']) ?>">
<h3><?= esc_html($atribs['titol']) ?></h3>
<?= wp_kses_post($contingut) ?>
<a href="<?= esc_url(get_permalink(get_page_by_path('contacte'))) ?>">
Anar al formulari →
</a>
</div>
<?php
return ob_get_clean();
});
?><?php
// Escapament de sortida (sempre usar la funció adequada al context)
esc_html($text); // text pla dins HTML
esc_attr($valor); // dins d'un atribut HTML (value="...")
esc_url($url); // URLs (href, src)
esc_js($valor); // dins de JavaScript
wp_kses_post($html); // HTML restringit (permet etiquetes segures)
absint($numero); // enter positiu o zero
// Comprovació de nonce (protecció CSRF)
// Generar
wp_nonce_field('accio_meva', 'nonce_field'); // camp ocult al formulari
wp_create_nonce('accio_meva'); // string del nonce
// Verificar
check_admin_referer('accio_meva', 'nonce_field'); // atura si invàlid
if (!wp_verify_nonce($_POST['nonce'], 'accio_ajax')) {
wp_die('Petició no autoritzada');
}
// Comprovar permisos
if (!current_user_can('manage_options')) {
wp_die('Accés denegat');
}
if (!current_user_can('edit_post', $post_id)) {
wp_die('No pots editar aquest post');
}
// Sanitització d'entrada
sanitize_text_field($_POST['nom']); // text pla
sanitize_email($_POST['email']); // email
sanitize_url($_POST['url']); // URL
absint($_POST['quantitat']); // enter positiu
wp_strip_all_tags($_POST['contingut']); // elimina etiquetes HTML
?><?php
// Activar WP_DEBUG per veure errors durant el desenvolupament
// wp-config.php
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false); // false en producció
// Comprovar errors de PHP al log
// Ubicació: wp-content/debug.log
?>Llista de verificació per a un tema o plugin:
# WP-CLI: interfície de línia d'ordres per a WordPress
wp plugin list # llistar plugins
wp plugin activate el-meu-plugin # activar
wp theme check el-meu-tema # comprovar el tema
# Theme Check plugin (panell WordPress)
# Verifica que el tema compleix els estàndards
# Query Monitor plugin
# Analitza les consultes a la BD, hooks usats i temps de càrregaUna bona documentació inclou:
Al codi:
<?php
/**
* Registra un nou usuari al sistema.
*
* @since 1.0.0
* @param string $nom Nom complet de l'usuari
* @param string $email Adreça de correu electrònic
* @param string $clau Contrasenya en text pla (es xifrarà internament)
* @return int|WP_Error ID del nou usuari o WP_Error en cas d'error
*/
function registra_usuari(string $nom, string $email, string $clau): int|WP_Error {
// ...
}
?>Fitxer readme.txt (format
WordPress):
=== El Meu Plugin ===
Contributors: ramonlopez
Tags: exemple, asix
Requires at least: 7.0
Tested up to: 7.0
Requires PHP: 8.2
Stable tag: 1.0.0
License: GPLv2 or later
== Descripció ==
Plugin d'exemple creat per al mòdul d'Implantació d'aplicacions web.
== Instal·lació ==
1. Puja el directori `el-meu-plugin` a `/wp-content/plugins/`
2. Activa el plugin des del panell d'administració
== Registre de canvis ==
= 1.0.0 =
* Primera versió
<?php
// Variables i tipus
$var = valor; var_dump($var); print_r($var);
(int) $v; (float) $v; (string) $v; (bool) $v;
// Condicionals
if ($c): elseif ($c2): else: endif;
match($v) { cas => resultat, default => ... };
// Bucles
while ($c) { }
do { } while ($c);
for ($i=0; $i<$n; $i++) { }
foreach ($arr as $k => $v) { }
// Funcions
function nom(tipus $param = defecte): tipusRetorn { return $val; }
fn($n) => expressió; // funció fletxa
// Classes
class Nom extends Pare implements Interfície {
public function __construct(private int $val) {}
public function mètode(): tipus { return $this->val; }
public static function estàtic(): void { self::$prop; }
}
// Errors
try { } catch (TipusException $e) { } finally { }
throw new Exception("missatge");
?>| Àrea | Funcions |
|---|---|
| Strings | strlen, mb_strlen, strpos, substr, str_replace, trim, strtolower, strtoupper, explode, implode, sprintf, htmlspecialchars, nl2br, strip_tags |
| Nombres | abs, ceil, floor, round, sqrt, pow, min, max, rand, random_int, number_format, is_numeric |
| Arrays | count, in_array, array_key_exists, array_push, array_pop, array_merge, array_slice, array_unique, array_filter, array_map, array_reduce, sort, usort, array_search, array_combine |
| Fitxers | file_get_contents, file_put_contents, fopen, fread, fwrite, fclose, file_exists, filesize, basename, pathinfo, scandir, is_dir, mkdir |
| BD (PDO) | new PDO(), prepare, execute, fetch, fetchAll, fetchColumn, rowCount, lastInsertId, beginTransaction, commit, rollBack |
| Seguretat | htmlspecialchars, filter_var, password_hash, password_verify, hash_equals, random_bytes, bin2hex |
| Sessió | session_start, session_regenerate_id, session_destroy, $_SESSION |
| Data | date, time, strtotime, new DateTime, DateInterval, date_default_timezone_set |
| JSON | json_encode, json_decode, json_last_error |
| Debug | var_dump, print_r, get_defined_vars, debug_backtrace, error_log |
| Àmbit | Pràctica |
|---|---|
| Codi | declare(strict_types=1) · Tipat de paràmetres i retorn
· PHPDoc |
| Noms | Classes: StudlyCaps · Mètodes/variables: camelCase · Constants: MAJÚSCULES |
| Fitxers | UTF-8 sense BOM · Una classe per fitxer · Ometre ?>
en fitxers 100% PHP |
| Seguretat | Sempre prepared statements · htmlspecialchars() en
sortides · password_hash() |
| Errors | display_errors=Off en producció · log_errors=On · Capturar excepcions |
| Rendiment | OPcache activat · Seleccionar columnes concretes · Paginació · Transaccions |
| WordPress | Sempre esc_*() per a sortides · Nonce per a formularis
· wp_enqueue_*() per a assets |