Построение модели в PHP для архитектуры MVC
Основы модели в MVC
Наиболее эффективное решение: абстрактная модель на PDO
Для построения модели в PHP в рамках MVC рекомендуется использовать абстрактный класс, инкапсулирующий подключение к базе данных через PDO и базовые CRUD-операции. Это обеспечивает единый механизм работы с данными, предотвращает дублирование кода и повышает безопасность за счет подготовленных запросов.
// Model.php
abstract class Model {
protected static $pdo;
public static function init() {
if (!static::$pdo) {
static::$pdo = new PDO('mysql:host=localhost;dbname=mvc_db', 'root', '', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]);
}
}
public static function all() {
static::init();
$table = static::getTableName();
$stmt = static::$pdo->query("SELECT * FROM {$table}");
return $stmt->fetchAll();
}
public static function find($id) {
static::init();
$table = static::getTableName();
$stmt = static::$pdo->prepare("SELECT * FROM {$table} WHERE id = :id");
$stmt->execute(['id' => $id]);
return $stmt->fetch();
}
public static function create($data) {
static::init();
$table = static::getTableName();
$columns = implode(', ', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$stmt = static::$pdo->prepare("INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})");
$stmt->execute($data);
return static::$pdo->lastInsertId();
}
abstract protected static function getTableName();
}Index php app forums controller (контроллер форума в php)
// User.php (конкретная модель)
class User extends Model {
protected static function getTableName() {
return 'users';
}
}Model index php (модель в php (mvc))
// Использование в контроллере
$users = User::all();
$user = User::find(1);
$newId = User::create(['name' => 'Иван', 'email' => 'ivan@example.com']);
Php controllers index (индексный контроллер php)
Проблема: дублирование подключения в каждом классе
Решение через абстрактный класс и статическую инициализацию. Типичная ошибка: забыть вызвать init() – приведет к ошибке Null. Рекомендуется сделать init() автоматическим через конструктор или магический метод __callStatic.
Вариант 1. Простая модель с прямыми SQL-запросами
Вопрос: как выполнить простой запрос к базе без лишних абстракций?
Этот подход оптимален для небольших проектов или единичных запросов. Пример:
// models/index.php
function getAllUsers() {
$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '');
$stmt = $pdo->query('SELECT * FROM users');
return $stmt->fetchAll();
}Проблема: SQL-инъекции при конкатенации строк
Использование query() с динамическими данными опасно. Решение: применять подготовленные запросы prepare()/execute().
// Безопасный вариант
function getUserById($id) {
$pdo = new PDO(...);
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => $id]);
return $stmt->fetch();
}Вариант 2. Использование ORM (например, Eloquent)
Вопрос: как упростить работу с базой и добавить отношения между моделями?
ORM автоматизирует маппинг таблиц в объекты. Пример с Eloquent вне Laravel:
// composer require illuminate/database
// config.php
use Illuminate\Database\Capsule\Manager as Capsule;
$capsule = new Capsule;
$capsule->addConnection(['driver' => 'mysql', 'host' => 'localhost', 'database' => 'test', 'username' => 'root', 'password' => '']);
$capsule->bootEloquent();
// User.php
use Illuminate\Database\Eloquent\Model;
class User extends Model {
protected $table = 'users';
}// Использование
$users = User::where('active', 1)->get();
$user = User::find(1);
$user->posts; // отношение hasManyПроблема: избыточность для простых проектов
ORM добавляет сложность конфигурации и может снижать производительность при неоптимальных запросах (N+1). Решение: использовать with() для жадной загрузки отношений.
Вариант 3. Паттерн Repository
Вопрос: как отделить логику запросов от модели и облегчить тестирование?
Repository выступает прослойкой между контроллером и моделью, сосредотачивая все запросы. Пример:
// UserRepository.php
class UserRepository {
private $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function findActiveUsers() {
$stmt = $this->pdo->prepare('SELECT * FROM users WHERE active = :active');
$stmt->execute(['active' => 1]);
return $stmt->fetchAll();
}
}// Контроллер
$pdo = new PDO(...);
$repo = new UserRepository($pdo);
$users = $repo->findActiveUsers();Проблема: увеличение количества классов
Для каждой модели может потребоваться свой репозиторий, что увеличивает код. Решение: создать общий абстрактный репозиторий.
Расширенные примеры работы с моделью
Модель с отношениями (User hasMany Post)
// Model.php (дополненный)
abstract class Model {
// ... базовая часть
public static function belongsTo($related, $foreignKey, $localKey) {
// реализация связи BelongsTo
}
public static function hasMany($related, $foreignKey, $localKey) {
// реализация связи HasMany
}
}
// User.php
class User extends Model {
protected static function getTableName() { return 'users'; }
public function posts() {
return $this->hasMany('Post', 'user_id', 'id');
}
}
// Post.php
class Post extends Model {
protected static function getTableName() { return 'posts'; }
}// Получение пользователя с постами (жадная загрузка)
$user = User::find(1);
$posts = $user->posts(); // возвращает массив постов (если реализовано)
Реализация метода findOrFail
public static function findOrFail($id) {
$result = static::find($id);
if (!$result) {
throw new \Exception('Record not found');
}
return $result;
}Транзакции в модели
public static function transferMoney($fromId, $toId, $amount) {
static::init();
static::$pdo->beginTransaction();
try {
// списываем
$stmt = static::$pdo->prepare('UPDATE accounts SET balance = balance - :amount WHERE id = :id');
$stmt->execute(['amount' => $amount, 'id' => $fromId]);
// зачисляем
$stmt = static::$pdo->prepare('UPDATE accounts SET balance = balance + :amount WHERE id = :id');
$stmt->execute(['amount' => $amount, 'id' => $toId]);
static::$pdo->commit();
} catch (\Exception $e) {
static::$pdo->rollBack();
throw $e;
}
}Мягкое удаление (Soft Delete)
public static function delete($id) {
static::init();
$table = static::getTableName();
$stmt = static::$pdo->prepare("UPDATE {$table} SET deleted_at = NOW() WHERE id = :id");
$stmt->execute(['id' => $id]);
}
public static function withTrashed() {
// переопределить all() для включения удаленных
}Использование Query Builder (простая реализация)
class QueryBuilder {
private $pdo;
private $query = '';
private $params = [];
public function select($columns) {
$this->query = "SELECT {$columns} ";
return $this;
}
public function from($table) {
$this->query .= "FROM {$table} ";
return $this;
}
public function where($column, $operator, $value) {
$this->query .= "WHERE {$column} {$operator} :{$column} ";
$this->params[$column] = $value;
return $this;
}
public function execute() {
$stmt = $this->pdo->prepare($this->query);
$stmt->execute($this->params);
return $stmt->fetchAll();
}
}// Пример использования
$builder = new QueryBuilder($pdo);
$users = $builder->select('*')->from('users')->where('active', '=', 1)->execute();