Newwebshop/classes/Model.php

620 lines
15 KiB
PHP

<?php
/**
* Model - Base class for all database models
* Extends ObjectModel with additional functionality
*/
abstract class Model extends ObjectModel
{
/** @var array Validation errors */
protected $errors = [];
/** @var array Validation rules */
protected static $rules = [];
/** @var array Relationships */
protected static $relationships = [];
/** @var array Fillable fields */
protected $fillable = [];
/** @var array Hidden fields */
protected $hidden = [];
/** @var array Casts */
protected $casts = [];
/** @var string Primary key */
protected $primaryKey = 'id';
/** @var bool Timestamps */
public $timestamps = true;
/** @var string Created at field */
protected $createdAtField = 'date_add';
/** @var string Updated at field */
protected $updatedAtField = 'date_upd';
/**
* Constructor
*
* @param int|null $id
* @param int|null $id_lang
* @param int|null $id_shop
*/
public function __construct($id = null, $id_lang = null, $id_shop = null)
{
parent::__construct($id, $id_lang, $id_shop);
$this->initializeModel();
}
/**
* Initialize model
*/
protected function initializeModel()
{
// Set default values
if ($this->timestamps) {
if (property_exists($this, $this->createdAtField)) {
$this->{$this->createdAtField} = date('Y-m-d H:i:s');
}
if (property_exists($this, $this->updatedAtField)) {
$this->{$this->updatedAtField} = date('Y-m-d H:i:s');
}
}
}
/**
* Create new model instance
*
* @param array $attributes
* @return static
*/
public static function create(array $attributes = [])
{
$model = new static();
$model->fill($attributes);
$model->save();
return $model;
}
/**
* Find model by ID
*
* @param int $id
* @return static|null
*/
public static function find($id)
{
return new static($id);
}
/**
* Find model by ID or throw exception
*
* @param int $id
* @return static
* @throws Exception
*/
public static function findOrFail($id)
{
$model = static::find($id);
if (!$model || !$model->id) {
throw new Exception('Model not found with ID: ' . $id);
}
return $model;
}
/**
* Find model by field
*
* @param string $field
* @param mixed $value
* @return static|null
*/
public static function findBy($field, $value)
{
$sql = 'SELECT * FROM `' . _DB_PREFIX_ . static::$definition['table'] . '` WHERE `' . pSQL($field) . '` = "' . pSQL($value) . '"';
$result = Db::getInstance()->getRow($sql);
if ($result) {
$model = new static();
$model->hydrate($result);
return $model;
}
return null;
}
/**
* Find model by field or throw exception
*
* @param string $field
* @param mixed $value
* @return static
* @throws Exception
*/
public static function findByOrFail($field, $value)
{
$model = static::findBy($field, $value);
if (!$model) {
throw new Exception('Model not found with ' . $field . ': ' . $value);
}
return $model;
}
/**
* Get all models
*
* @param string $orderBy
* @param string $orderWay
* @return array
*/
public static function all($orderBy = null, $orderWay = 'ASC')
{
$sql = 'SELECT * FROM `' . _DB_PREFIX_ . static::$definition['table'] . '`';
if ($orderBy) {
$sql .= ' ORDER BY `' . pSQL($orderBy) . '` ' . pSQL($orderWay);
}
$results = Db::getInstance()->executeS($sql);
$models = [];
foreach ($results as $result) {
$model = new static();
$model->hydrate($result);
$models[] = $model;
}
return $models;
}
/**
* Get models with pagination
*
* @param int $page
* @param int $perPage
* @param string $orderBy
* @param string $orderWay
* @return array
*/
public static function paginate($page = 1, $perPage = 20, $orderBy = null, $orderWay = 'ASC')
{
$offset = ($page - 1) * $perPage;
$sql = 'SELECT * FROM `' . _DB_PREFIX_ . static::$definition['table'] . '`';
if ($orderBy) {
$sql .= ' ORDER BY `' . pSQL($orderBy) . '` ' . pSQL($orderWay);
}
$sql .= ' LIMIT ' . (int) $perPage . ' OFFSET ' . (int) $offset;
$results = Db::getInstance()->executeS($sql);
$models = [];
foreach ($results as $result) {
$model = new static();
$model->hydrate($result);
$models[] = $model;
}
return $models;
}
/**
* Count total models
*
* @return int
*/
public static function count()
{
$sql = 'SELECT COUNT(*) as total FROM `' . _DB_PREFIX_ . static::$definition['table'] . '`';
$result = Db::getInstance()->getRow($sql);
return (int) $result['total'];
}
/**
* Check if model exists
*
* @param int $id
* @return bool
*/
public static function exists($id)
{
$sql = 'SELECT COUNT(*) as total FROM `' . _DB_PREFIX_ . static::$definition['table'] . '` WHERE `' . static::$definition['primary'] . '` = ' . (int) $id;
$result = Db::getInstance()->getRow($sql);
return (int) $result['total'] > 0;
}
/**
* Fill model with attributes
*
* @param array $attributes
* @return $this
*/
public function fill(array $attributes)
{
foreach ($attributes as $key => $value) {
if (in_array($key, $this->fillable) || empty($this->fillable)) {
$this->{$key} = $value;
}
}
return $this;
}
/**
* Update model
*
* @param array $attributes
* @return bool
*/
public function update(array $attributes = [])
{
$this->fill($attributes);
if ($this->timestamps && property_exists($this, $this->updatedAtField)) {
$this->{$this->updatedAtField} = date('Y-m-d H:i:s');
}
return parent::update();
}
/**
* Delete model
*
* @return bool
*/
public function delete()
{
return parent::delete();
}
/**
* Validate model
*
* @param bool $die
* @param bool $error_return
* @return bool|string
*/
public function validate($die = true, $error_return = false)
{
$this->errors = [];
// Validate required fields
foreach (static::$definition['fields'] as $field => $rules) {
if (isset($rules['required']) && $rules['required']) {
if (!isset($this->{$field}) || empty($this->{$field})) {
$this->errors[] = 'Field ' . $field . ' is required';
}
}
}
// Validate custom rules
if (!empty(static::$rules)) {
foreach (static::$rules as $field => $rules) {
if (isset($this->{$field})) {
$this->validateField($field, $this->{$field}, $rules);
}
}
}
if (!empty($this->errors)) {
if ($die) {
throw new Exception(implode(', ', $this->errors));
}
return $error_return ? implode(', ', $this->errors) : false;
}
return true;
}
/**
* Validate single field
*
* @param string $field
* @param mixed $value
* @param array $rules
*/
protected function validateField($field, $value, $rules)
{
foreach ($rules as $rule) {
switch ($rule) {
case 'email':
if (!Validate::isEmail($value)) {
$this->errors[] = 'Field ' . $field . ' must be a valid email';
}
break;
case 'url':
if (!Validate::isUrl($value)) {
$this->errors[] = 'Field ' . $field . ' must be a valid URL';
}
break;
case 'numeric':
if (!is_numeric($value)) {
$this->errors[] = 'Field ' . $field . ' must be numeric';
}
break;
case 'integer':
if (!is_int($value) && !ctype_digit($value)) {
$this->errors[] = 'Field ' . $field . ' must be an integer';
}
break;
case 'boolean':
if (!is_bool($value) && !in_array($value, [0, 1, '0', '1'])) {
$this->errors[] = 'Field ' . $field . ' must be boolean';
}
break;
}
}
}
/**
* Get validation errors
*
* @return array
*/
public function getValidationErrors()
{
return $this->errors;
}
/**
* Check if model has errors
*
* @return bool
*/
public function hasErrors()
{
return !empty($this->errors);
}
/**
* Get model as array
*
* @return array
*/
public function toArray()
{
$array = [];
foreach (get_object_vars($this) as $key => $value) {
if (!in_array($key, $this->hidden)) {
$array[$key] = $value;
}
}
return $array;
}
/**
* Get model as JSON
*
* @return string
*/
public function toJson()
{
return json_encode($this->toArray());
}
/**
* Define one-to-one relationship
*
* @param string $related
* @param string $foreignKey
* @param string $localKey
* @return mixed
*/
public function hasOne($related, $foreignKey = null, $localKey = null)
{
$foreignKey = $foreignKey ?: $this->getForeignKey();
$localKey = $localKey ?: $this->getKeyName();
$relatedModel = new $related();
return $relatedModel->findBy($foreignKey, $this->{$localKey});
}
/**
* Define one-to-many relationship
*
* @param string $related
* @param string $foreignKey
* @param string $localKey
* @return array
*/
public function hasMany($related, $foreignKey = null, $localKey = null)
{
$foreignKey = $foreignKey ?: $this->getForeignKey();
$localKey = $localKey ?: $this->getKeyName();
$relatedModel = new $related();
$sql = 'SELECT * FROM `' . _DB_PREFIX_ . $relatedModel::$definition['table'] . '` WHERE `' . pSQL($foreignKey) . '` = ' . (int) $this->{$localKey};
$results = Db::getInstance()->executeS($sql);
$models = [];
foreach ($results as $result) {
$model = new $related();
$model->hydrate($result);
$models[] = $model;
}
return $models;
}
/**
* Define belongs-to relationship
*
* @param string $related
* @param string $foreignKey
* @param string $ownerKey
* @return mixed
*/
public function belongsTo($related, $foreignKey = null, $ownerKey = null)
{
$foreignKey = $foreignKey ?: $this->getForeignKey();
$ownerKey = $ownerKey ?: 'id';
$relatedModel = new $related();
return $relatedModel->find($this->{$foreignKey});
}
/**
* Define many-to-many relationship
*
* @param string $related
* @param string $table
* @param string $foreignPivotKey
* @param string $relatedPivotKey
* @param string $parentKey
* @param string $relatedKey
* @return array
*/
public function belongsToMany($related, $table = null, $foreignPivotKey = null, $relatedPivotKey = null, $parentKey = null, $relatedKey = null)
{
$table = $table ?: $this->joiningTable($related);
$foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey();
$relatedPivotKey = $relatedPivotKey ?: $this->getRelatedForeignKey($related);
$parentKey = $parentKey ?: $this->getKeyName();
$relatedKey = $relatedKey ?: 'id';
$relatedModel = new $related();
$sql = 'SELECT r.* FROM `' . _DB_PREFIX_ . $table . '` p
JOIN `' . _DB_PREFIX_ . $relatedModel::$definition['table'] . '` r ON p.`' . pSQL($relatedPivotKey) . '` = r.`' . pSQL($relatedKey) . '`
WHERE p.`' . pSQL($foreignPivotKey) . '` = ' . (int) $this->{$parentKey};
$results = Db::getInstance()->executeS($sql);
$models = [];
foreach ($results as $result) {
$model = new $related();
$model->hydrate($result);
$models[] = $model;
}
return $models;
}
/**
* Get foreign key name
*
* @return string
*/
protected function getForeignKey()
{
return strtolower(class_basename($this)) . '_id';
}
/**
* Get related foreign key name
*
* @param string $related
* @return string
*/
protected function getRelatedForeignKey($related)
{
return strtolower(class_basename($related)) . '_id';
}
/**
* Get joining table name
*
* @param string $related
* @return string
*/
protected function joiningTable($related)
{
$models = [strtolower(class_basename($this)), strtolower(class_basename($related))];
sort($models);
return implode('_', $models);
}
/**
* Get key name
*
* @return string
*/
public function getKeyName()
{
return $this->primaryKey;
}
/**
* Get key value
*
* @return mixed
*/
public function getKey()
{
return $this->{$this->getKeyName()};
}
/**
* Check if model is new
*
* @return bool
*/
public function isNew()
{
return !$this->getKey();
}
/**
* Refresh model from database
*
* @return $this
*/
public function refresh()
{
if ($this->getKey()) {
$fresh = static::find($this->getKey());
if ($fresh) {
$this->hydrate($fresh->toArray());
}
}
return $this;
}
/**
* Touch model (update timestamps)
*
* @return bool
*/
public function touch()
{
if ($this->timestamps && property_exists($this, $this->updatedAtField)) {
$this->{$this->updatedAtField} = date('Y-m-d H:i:s');
return $this->update();
}
return false;
}
/**
* Get table name
*
* @return string
*/
public function getTable()
{
return static::$definition['table'];
}
/**
* Get connection
*
* @return Db
*/
public function getConnection()
{
return Db::getInstance();
}
}