Modern PHP Engineering
Chapitre 5
05 — Bases de Données PHP
05 — Bases de Données PHP
Cours 05 — Bases de Données PHP
05.1 PDO — PHP Data Objects
PDO est l'extension native PHP pour l'accès aux bases de données. Elle supporte MySQL, PostgreSQL, SQLite, Oracle, MSSQL, etc.
Connexion
<?php
declare(strict_types=1);
// Connexion MySQL
$dsn = 'mysql:host=localhost;port=3306;dbname=app_db;charset=utf8mb4';
$username = 'root';
$password = 'secret';
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci",
];
$pdo = new PDO($dsn, $username, $password, $options);
// Connexion PostgreSQL
$dsn = 'pgsql:host=localhost;port=5432;dbname=app_db';
$pdo = new PDO($dsn, $username, $password, $options);
// Connexion SQLite
$pdo = new PDO('sqlite:/path/to/database.sqlite');
Prepared statements
<?php
declare(strict_types=1);
// SELECT avec paramètres nommés
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND active = :active');
$stmt->execute(['email' => 'alice@example.com', 'active' => true]);
$user = $stmt->fetch(); // false si pas de résultat
// SELECT avec paramètres positionnels
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([42]);
$user = $stmt->fetch();
// Fetch modes
$users = $stmt->fetchAll(PDO::FETCH_ASSOC); // Tableau associatif
$user = $stmt->fetch(PDO::FETCH_OBJ); // Objet stdClass
$user = $stmt->fetch(PDO::FETCH_CLASS, User::class); // Hydratation en classe
$count = $stmt->fetchColumn(); // Une seule colonne
$pairs = $stmt->fetchAll(PDO::FETCH_KEY_PAIR); // [key => value]
// INSERT
$stmt = $pdo->prepare('INSERT INTO users (name, email) VALUES (:name, :email)');
$stmt->execute(['name' => 'Alice', 'email' => 'alice@example.com']);
$lastId = $pdo->lastInsertId();
// UPDATE
$stmt = $pdo->prepare('UPDATE users SET name = :name WHERE id = :id');
$stmt->execute(['name' => 'Alice Updated', 'id' => 1]);
$affected = $stmt->rowCount(); // Nombre de lignes affectées
// DELETE
$stmt = $pdo->prepare('DELETE FROM users WHERE id = :id');
$stmt->execute(['id' => 1]);
Transactions
<?php
declare(strict_types=1);
try {
$pdo->beginTransaction();
$stmt = $pdo->prepare('UPDATE accounts SET balance = balance - :amount WHERE id = :from');
$stmt->execute(['amount' => 100, 'from' => 1]);
$stmt = $pdo->prepare('UPDATE accounts SET balance = balance + :amount WHERE id = :to');
$stmt->execute(['amount' => 100, 'to' => 2]);
$pdo->commit();
} catch (PDOException $e) {
$pdo->rollBack();
log_error("Transaction échouée : " . $e->getMessage());
throw $e;
}
Error modes
<?php
// Mode silencieux (défaut) — vérifier errorCode()
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
$stmt = $pdo->query('SELECT *');
if ($stmt === false) {
$error = $pdo->errorInfo();
}
// Mode exception (RECOMMANDÉ)
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
try {
$pdo->query('SELECT *');
} catch (PDOException $e) {
echo $e->getMessage(); // "SQLSTATE[42000]: ..."
}
// Mode warning
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
Repository pattern avec PDO
<?php
declare(strict_types=1);
class UserRepository
{
public function __construct(
private readonly PDO $pdo,
) {}
public function findById(int $id): ?array
{
$stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$id]);
$result = $stmt->fetch();
return $result ?: null;
}
public function findByEmail(string $email): ?array
{
$stmt = $this->pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$email]);
$result = $stmt->fetch();
return $result ?: null;
}
public function findAll(int $page = 1, int $perPage = 20): array
{
$offset = ($page - 1) * $perPage;
$stmt = $this->pdo->prepare(
'SELECT * FROM users ORDER BY id LIMIT ? OFFSET ?'
);
$stmt->execute([$perPage, $offset]);
return $stmt->fetchAll();
}
public function create(array $data): int
{
$columns = implode(', ', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$stmt = $this->pdo->prepare(
"INSERT INTO users ($columns) VALUES ($placeholders)"
);
$stmt->execute($data);
return (int) $this->pdo->lastInsertId();
}
public function update(int $id, array $data): bool
{
$sets = implode(', ', array_map(
fn(string $col): string => "$col = :$col",
array_keys($data)
));
$stmt = $this->pdo->prepare(
"UPDATE users SET $sets WHERE id = :id"
);
$stmt->execute([...$data, 'id' => $id]);
return $stmt->rowCount() > 0;
}
public function delete(int $id): bool
{
$stmt = $this->pdo->prepare('DELETE FROM users WHERE id = ?');
$stmt->execute([$id]);
return $stmt->rowCount() > 0;
}
}
05.2 Eloquent ORM
Eloquent est l'ORM de Laravel. Il suit le pattern Active Record.
Modèles
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class User extends Model
{
use SoftDeletes; // Suppression logique (deleted_at)
protected $table = 'users';
protected $primaryKey = 'id';
public $timestamps = true; // created_at, updated_at
protected $fillable = [
'name', 'email', 'password', 'role_id',
];
protected $hidden = [
'password', 'remember_token',
];
protected $casts = [
'email_verified_at' => 'datetime',
'is_admin' => 'boolean',
'settings' => 'array', // JSON → array PHP
'options' => 'collection',
];
}
// Utilisation
$user = new User();
$user->name = 'Alice';
$user->email = 'alice@example.com';
$user->save();
User::create([
'name' => 'Bob',
'email' => 'bob@example.com',
]);
$user = User::find(1);
$user->update(['name' => 'Alice Updated']);
$user->delete();
$users = User::where('active', true)
->where('role_id', '>', 1)
->orderBy('name')
->paginate(20);
Relations
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations;
class User extends Model
{
// Une-à-un
public function profile(): Relations\HasOne
{
return $this->hasOne(Profile::class);
}
// Une-à-plusieurs
public function posts(): Relations\HasMany
{
return $this->hasMany(Post::class);
}
// Appartient-à
public function role(): Relations\BelongsTo
{
return $this->belongsTo(Role::class);
}
// Plusieurs-à-plusieurs
public function roles(): Relations\BelongsToMany
{
return $this->belongsToMany(Role::class)
->withPivot('assigned_at')
->withTimestamps();
}
// HasManyThrough
public function comments(): Relations\HasManyThrough
{
return $this->hasManyThrough(Comment::class, Post::class);
}
// Morph (polymorphique)
public function images(): Relations\MorphMany
{
return $this->morphMany(Image::class, 'imageable');
}
}
// Chargement
$user = User::with('posts', 'profile')->find(1);
$user->load('roles');
// Chargement conditionnel
$user = User::with(['posts' => fn($q) => $q->where('published', true)])->find(1);
// Eager loading par défaut
class User extends Model {
protected $with = ['profile'];
}
Scopes
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
class User extends Model
{
// Scope local
public function scopeActive(Builder $query): Builder
{
return $query->where('active', true);
}
public function scopeByRole(Builder $query, string $role): Builder
{
return $query->whereHas('role', fn($q) => $q->where('name', $role));
}
public function scopeRecentlyCreated(Builder $query): Builder
{
return $query->where('created_at', '>=', now()->subDays(7));
}
}
// Utilisation
$admins = User::active()
->byRole('admin')
->recentlyCreated()
->get();
Accessors et Mutators
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
class User extends Model
{
// Accessor (lecture)
public function getNameAttribute(string $value): string
{
return ucfirst($value);
}
// Mutator (écriture)
public function setPasswordAttribute(string $value): void
{
$this->attributes['password'] = bcrypt($value);
}
// Accessor/Mutator moderne (PHP 8+)
protected function fullName(): Attribute
{
return Attribute::make(
get: fn(mixed $value, array $attributes) =>
"{$attributes['first_name']} {$attributes['last_name']}",
set: fn(string $value) => [
'first_name' => explode(' ', $value)[0],
'last_name' => explode(' ', $value)[1] ?? '',
],
);
}
}
// Utilisation
$user = User::find(1);
echo $user->full_name; // Accessor
$user->full_name = 'Alice Martin'; // Mutator
$user->save();
Events
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
protected $dispatchesEvents = [
'created' => UserCreated::class,
'updated' => UserUpdated::class,
'deleted' => UserDeleted::class,
];
// Ou avec les boot methods
protected static function booted(): void
{
static::creating(function (User $user) {
$user->uuid = (string) str()->uuid();
});
static::created(function (User $user) {
Log::info('Utilisateur créé', ['id' => $user->id]);
});
}
}
Factories
<?php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class UserFactory extends Factory
{
protected $model = User::class;
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'password' => bcrypt('password'),
'active' => fake()->boolean(80),
];
}
public function admin(): static
{
return $this->state(fn(array $attributes) => [
'role_id' => Role::where('name', 'admin')->first()->id,
]);
}
}
// Utilisation
User::factory()->count(10)->create();
User::factory()->admin()->create();
05.3 Doctrine ORM
Doctrine est l'ORM de Symfony. Il suit le pattern Data Mapper.
Entités
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: 'users')]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private int $id;
#[ORM\Column(type: 'string', length: 255)]
private string $name;
#[ORM\Column(type: 'string', length: 255, unique: true)]
private string $email;
#[ORM\Column(type: 'datetime_immutable')]
private \DateTimeImmutable $createdAt;
#[ORM\ManyToOne(targetEntity: Role::class, inversedBy: 'users')]
private ?Role $role = null;
public function __construct()
{
$this->createdAt = new \DateTimeImmutable();
}
// Getters/Setters...
}
DQL (Doctrine Query Language)
<?php
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
class UserRepository
{
public function __construct(
private readonly EntityManagerInterface $em,
) {}
public function findActiveAdmins(): array
{
$dql = 'SELECT u FROM App\Entity\User u
JOIN u.role r
WHERE r.name = :roleName
AND u.active = :active
ORDER BY u.name ASC';
$query = $this->em->createQuery($dql);
$query->setParameters([
'roleName' => 'admin',
'active' => true,
]);
return $query->getResult();
}
public function findRecentUsers(int $days = 7): array
{
$qb = $this->em->createQueryBuilder();
$qb->select('u')
->from(User::class, 'u')
->where('u.createdAt >= :date')
->setParameter('date', new \DateTimeImmutable("-{$days} days"))
->orderBy('u.createdAt', 'DESC');
return $qb->getQuery()->getResult();
}
}
Migrations Doctrine
# Générer une migration
php bin/console doctrine:migrations:diff
# Exécuter les migrations
php bin/console doctrine:migrations:migrate
# Statut
php bin/console doctrine:migrations:status
# Rollback
php bin/console doctrine:migrations:migrate prev
05.4 Migrations
Phinx
<?php
// db/migrations/20250729000001_create_users.php
use Phinx\Migration\AbstractMigration;
class CreateUsers extends AbstractMigration
{
public function up(): void
{
$table = $this->table('users', ['id' => false, 'primary_key' => ['id']]);
$table->addColumn('id', 'biginteger', ['identity' => true])
->addColumn('name', 'string', ['limit' => 255])
->addColumn('email', 'string', ['limit' => 255])
->addColumn('password', 'string', ['limit' => 255])
->addColumn('active', 'boolean', ['default' => true])
->addColumn('created_at', 'datetime')
->addColumn('updated_at', 'datetime')
->addIndex(['email'], ['unique' => true])
->create();
}
public function down(): void
{
$this->table('users')->drop()->save();
}
}
Laravel Migrations
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->boolean('active')->default(true);
$table->foreignId('role_id')->constrained();
$table->timestamps();
$table->softDeletes();
});
}
public function down(): void
{
Schema::dropIfExists('users');
}
};
05.5 Redis avec PHP
Prédi (PHP pur)
composer require predis/predis
<?php
use Predis\Client as Redis;
$redis = new Redis([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => 6379,
'password' => null,
'database' => 0,
]);
// String
$redis->set('user:1:name', 'Alice');
echo $redis->get('user:1:name');
// Avec TTL
$redis->setex('session:token', 3600, 'abc123');
// Lists
$redis->lpush('queue:emails', 'email1', 'email2');
$email = $redis->rpop('queue:emails');
// Sets
$redis->sadd('user:1:roles', 'admin', 'editor');
$roles = $redis->smembers('user:1:roles');
// Hashes
$redis->hmset('user:1', ['name' => 'Alice', 'email' => 'alice@example.com']);
echo $redis->hget('user:1', 'name');
// Sorted sets
$redis->zadd('leaderboard', 100, 'user:1');
$top = $redis->zrevrange('leaderboard', 0, 9, 'WITHSCORES');
// Cache pattern
$cacheKey = "user:{$id}";
$user = $redis->get($cacheKey);
if (!$user) {
$user = $database->find($id);
$redis->setex($cacheKey, 3600, serialize($user));
} else {
$user = unserialize($user);
}
PhpRedis (extension C)
<?php
// Plus performant que predis (extension native)
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->auth('password');
$redis->set('key', 'value');
$redis->setex('key', 3600, 'value');
Exercices
- Créez un repository PDO avec pagination et filtres
- Modélisez des relations Eloquent (User, Post, Comment, Tag)
- Écrivez une requête Doctrine DQL complexe avec jointures
- Créez une migration Laravel avec index et foreign keys
- Implémentez un cache Redis pour les résultats de requêtes
Références
- php.net/manual/fr/book.pdo.php
- laravel.com/docs/eloquent
- doctrine-project.org
- redis.io/documentation
- book.cakephp.org/phinx