. */ /** * Thrown in Model when a table row could not be found */ class ModelNotFoundException extends Exception { } /** * Thrown in Model when a column cannot be edited */ class ModelIllegalAccessException extends Exception { } /** * Thrown in Model when a call to __set() fails */ class ModelSetFailedException extends Exception { } /** * An abstract interface to a database table */ abstract class Model { /** * @var string $table The database table * @var string $primary_key The table's primary key * @var string[] $protected_columns Columns that cannot be edited * @var string[] $fillable_columns Columns that can be edited * @var string[] $timestamps Columns that are TIMESTAMPs (special treatment in accessor and mutator) * @var string[] $dates Columns that are DATEs (special treatment in accessor and mutator) * @var string[] $booleans Columns that are BOOLEANs (special treatment in accessor and mutator) */ public static $table = '', $primary_key = 'id', $protected_columns = ['id'], $fillable_columns = [], $timestamps = [], $dates = [], $booleans = []; /** * @var PDO $pdo A PDO instance for database communication * @var mixed[] $data The column values */ protected $pdo, $data; /** * Create a new instance * * @param PDO $pdo The PDO class, to access the database * @param int $id The id of the row to fetch * * @throws PDOException If something went wrong with the database * @throws ModelNotFoundException If the id could not be found */ public function __construct($pdo, $id) { $this->pdo = $pdo; $stmt = $this->pdo->prepare("SELECT * FROM `".self::table()."` WHERE `".static::$primary_key."`=?"); $stmt->execute([$id]); if ($stmt->rowCount() == 0) { throw new ModelNotFoundException("The ".static::$table." with id '$id' could not be found."); } $this->data = $stmt->fetch(PDO::FETCH_ASSOC); } /** * Set a column value * * @param string $key The column * @param mixed $value The value * * @throws PDOException Database error */ public function __set($key, $value) { if (!in_array($key, static::$fillable_columns)) { throw new ModelIllegalAccessException("Column `".self::table()."`.`$key` cannot be edited."); } if ($this->data[$key] == $value) { return; } $stmt = $this->pdo->prepare("UPDATE `".self::table()."` SET `$key`=? WHERE `".static::$primary_key."`=?"); $stmt->execute([ $this->mutator($key, $value), $this->data[static::$primary_key] ]); if ($stmt->rowCount() != 1) { throw new ModelEditFailedException("Failed to update `".self::table()."`.`$key` to '$value'."); } $this->data[$key] = $value; } /** * Get a column value * * @param string $key The column * * @return mixed The value */ public function __get($key) { return $this->accessor($key, $this->data[$key]); } /** * Create a new row * * @param PDO $pdo Database connection * @param mixed[] $values The column values, in the order of $fillable_columns * * @throws PDOException Database error * * @return self The new item */ public static function create($pdo, $values) { $columns = array_combine(static::$fillable_columns, $values); $questions = []; $class = get_called_class(); foreach ($columns as $column => $value) { $columns[$column] = $class::mutator($column, $value); $questions[] = '?'; } $stmt = $pdo->prepare( "INSERT INTO `".self::table()."` " . "(`" . implode('`, `', array_keys($columns)) . "`) " . "VALUES (" . implode(',', $questions) . ")"); $stmt->execute(array_values($columns)); return new $class($pdo, $pdo->lastInsertId()); } /** * Post-__get() hook to modify the value * * @param string $key The column * @param string $value The value * * @return mixed The modified value */ protected static function accessor($key, $value) { if (is_null($value)) { return null; } elseif (in_array($key, static::$booleans)) { return (bool) $value; } elseif (in_array($key, static::$dates) || in_array($key, static::$timestamps)) { return strtotime($value); } else { return $value; } } /** * Pre-__set() hook to modify a value * * @param string $key The column * @param mixed $value The value * * @return string The modified value */ protected static function mutator($key, $value) { if (in_array($key, static::$dates) && is_int($value)) { return date('Y-m-d', $value); } elseif (in_array($key, static::$timestamps) && is_int($value)) { return date('Y-m-d H:i:s', $value); } else { return (string) $value; } } /** * Delete the row * * @throws PDOException Database error * * @return bool True iff the row was really deleted */ public function delete() { $stmt = $this->pdo->prepare("DELETE FROM `".self::table()."` WHERE `".static::$primary_key."`=?"); $stmt->execute([$this->data[static::$primary_key]]); return $stmt->rowCount() != 0; } /** * The actual table, after adding prefixes and the like * * @return string The database table */ private static function table() { return Constants::db_prefix . static::$table; } }