620 lines
15 KiB
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();
|
|
}
|
|
}
|