You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
557 lines
12 KiB
557 lines
12 KiB
<?php |
|
|
|
namespace App\Model; |
|
|
|
use App\Classes\Db; |
|
|
|
abstract class Model { |
|
|
|
protected $table; |
|
|
|
protected $primaryKey = 'id'; |
|
|
|
protected $keyType = 'int'; |
|
|
|
protected $incrementing = true; |
|
|
|
protected $perPage = 20; |
|
|
|
protected $exists = false; |
|
|
|
protected $sort = 'id'; |
|
|
|
const CREATED_AT = 'created_at'; |
|
|
|
const UPDATED_AT = 'updated_at'; |
|
|
|
private $attributes = []; |
|
|
|
public function __construct() |
|
{ |
|
// Fill in table name |
|
if (empty($this->table)) { |
|
$class = strtolower(get_class($this)); |
|
$pos = strrpos($class, '\\'); |
|
$this->table = substr($class, $pos + 1); |
|
} |
|
|
|
// Pull columns from cache |
|
$columnsCache = Db::getColumns(); |
|
|
|
// If exists in cache |
|
if (array_key_exists($this->table, $columnsCache)) { |
|
$columns = $columnsCache[$this->table]; |
|
} |
|
// Otherwise query Db and add to cache |
|
else { |
|
$columns = self::query("SHOW COLUMNS FROM `$this->table`"); |
|
$columns = array_column($columns, 'Field'); |
|
$columnsCache[$this->table] = $columns; |
|
Db::setColumns($columnsCache); |
|
} |
|
|
|
// Create attribute placeholders |
|
if (_exists($columns)) { |
|
foreach ($columns as $column) { |
|
if ($column != $this->primaryKey) { |
|
$this->attributes[] = $column; |
|
} |
|
$this->{$column} = null; |
|
} |
|
} |
|
} |
|
|
|
//-------------------------------------// |
|
|
|
/** |
|
* Retreive data via PDO prepared statements |
|
* |
|
* The most frequently used constants for PDO are listed below, |
|
* find more at: {@link https://www.php.net/manual/en/pdo.constants.php} |
|
* - PDO::PARAM_BOOL |
|
* - PDO::PARAM_NULL |
|
* - PDO::PARAM_INT |
|
* - PDO::PARAM_STR |
|
* |
|
* Usage: |
|
* self::query( |
|
* "SELECT * FROM `example` WHERE `id` = :id AND `number` = :number AND `text` = :text", [ |
|
* [':id', 1], |
|
* [':number', 7, \PDO::PARAM_INT], |
|
* [':text', 'A random string', \PDO::PARAM_STR], |
|
* ]); |
|
* |
|
* self::query( |
|
* 'SELECT * FROM `example` WHERE `id` IN (?, ?, ?) AND `thing` = ?, [ |
|
* 1, 2, 3, 'stuff' |
|
* ], |
|
* '?' |
|
* ); |
|
* |
|
* @param $query The full prepared query statement |
|
* @param $parameters The values to insert into the prepared statement |
|
* @param $type Type of prepared statement, ':' for named placeholders, |
|
* '?' for value placeholders |
|
* |
|
* @return array|null Retreived data, or null |
|
*/ |
|
protected static function query(string $query, array $parameters = [], |
|
$type = ':'): ?array |
|
{ |
|
if ($type == '?' && substr_count($query, $type) != count($parameters)) { |
|
return null; |
|
} |
|
|
|
$query = Db::get()->prepare($query); |
|
|
|
$success = false; |
|
if ($type == '?') { |
|
$success = $query->execute($parameters); |
|
} |
|
else { |
|
foreach ($parameters as $key => $parameter) { |
|
if (count($parameter) == 2) { |
|
$query->bindParam($parameter[0], $parameter[1]); |
|
} |
|
else if (count($parameter) == 3) { |
|
$query->bindParam($parameter[0], $parameter[1], $parameter[2]); |
|
} |
|
} |
|
$success = $query->execute(); |
|
} |
|
|
|
if (!$success) { |
|
return null; |
|
} |
|
|
|
return $query->fetchAll(); |
|
} |
|
|
|
//-------------------------------------// |
|
|
|
public function exists(): bool |
|
{ |
|
if ($this->exists == true) { |
|
return $this->exists; |
|
} |
|
|
|
$exists = self::query( |
|
"SELECT id FROM `$this->table` WHERE `$this->primaryKey` = :id", [ |
|
[':id', $this->{$this->primaryKey}], |
|
] |
|
); |
|
|
|
if (_exists($exists)) { |
|
$this->exists = true; |
|
} |
|
|
|
return $this->exists; |
|
} |
|
|
|
public function save(): bool |
|
{ |
|
$parameters = []; |
|
|
|
$exists = $this->exists(); |
|
// Insert new Model |
|
if (!$exists) { |
|
$query = "INSERT INTO `$this->table` "; |
|
for ($i = 0; $i < 2; $i++) { |
|
if ($i == 0) { |
|
$query .= '('; |
|
} |
|
else { |
|
$query .= 'VALUES ('; |
|
} |
|
|
|
foreach ($this->attributes as $key => $attribute) { |
|
if ($key != 0) { |
|
$query .= ', '; |
|
} |
|
|
|
if ($i == 0) { |
|
$query .= "`$attribute`"; |
|
} |
|
else { |
|
$query .= ":$attribute"; |
|
$parameters[] = [":$attribute", $this->{$attribute}]; |
|
} |
|
} |
|
$query .= ') '; |
|
} |
|
} |
|
// Update existing Model |
|
else { |
|
$query = "UPDATE `$this->table` SET "; |
|
foreach ($this->attributes as $key => $attribute) { |
|
if ($key != 0) { |
|
$query .= ', '; |
|
} |
|
|
|
$query .= "`$attribute` = :$attribute"; |
|
$parameters[] = [":$attribute", $this->{$attribute}]; |
|
} |
|
$query .= " WHERE `$this->primaryKey` = :id"; |
|
$parameters[] = [':id', $this->{$this->primaryKey}]; |
|
} |
|
|
|
if (self::query($query, $parameters) === null) { |
|
return false; |
|
} |
|
|
|
// Fill in primary key and exists for newly created Models |
|
if (!$exists) { |
|
$id = self::query("SELECT LAST_INSERT_ID() as `$this->primaryKey`"); |
|
if (_exists($id)) { |
|
$this->{$this->primaryKey} = $id[0][$this->primaryKey]; |
|
$this->exists = true; |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
public function delete(): bool |
|
{ |
|
return self::query( |
|
"DELETE FROM `$this->table` WHERE `$this->primaryKey` = :id", [ |
|
[':id', $this->{$this->primaryKey}], |
|
] |
|
) !== null; |
|
} |
|
|
|
public function count(): int |
|
{ |
|
$total = self::query( |
|
"SELECT COUNT({$this->primaryKey}) as total FROM `$this->table`"); |
|
return $total[0]['total'] ?? 0; |
|
} |
|
|
|
//-------------------------------------// |
|
|
|
public function fill(array $fill): bool |
|
{ |
|
if (!_exists([$fill])) { |
|
return false; |
|
} |
|
|
|
// Set primary key |
|
if (_exists($fill, $this->primaryKey)) { |
|
$this->{$this->primaryKey} = $fill[$this->primaryKey]; |
|
} |
|
|
|
// Set other attributes |
|
foreach ($this->getAttributes() as $attribute) { |
|
if (isset($fill[$attribute])) { |
|
// Escape sequences are only interpreted with double quotes! |
|
$this->{$attribute} = preg_replace('/\r\n?/', "\n", $fill[$attribute]); |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
public function validate(): bool |
|
{ |
|
foreach ($this->getAttributes() as $attribute) { |
|
|
|
$required = false; |
|
foreach ($this->rules as $rule) { |
|
if ($rule[0] == $attribute && $rule[2] == 1 && $rule[3] == 0) { |
|
$required = true; |
|
break; |
|
} |
|
} |
|
|
|
// Exit if rule is marked 'required' but empty, "0" is not empty! |
|
if ($required && empty($this->{$attribute}) && $this->{$attribute} !== "0") { |
|
return false; |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
public function getPrimaryKey(): string |
|
{ |
|
return $this->primaryKey; |
|
} |
|
|
|
public function getAttributes(): array |
|
{ |
|
return $this->attributes; |
|
} |
|
|
|
public function getAttributesRules(): array |
|
{ |
|
|
|
// Check if model has set rules |
|
if (!_exists($this->rules) || !is_array($this->rules)) { |
|
$rules = []; |
|
} |
|
else { |
|
$rules = $this->rules; |
|
} |
|
|
|
// Get first column (name) of every rule |
|
$rulesSearch = array_column($rules, 0); |
|
|
|
// Loop through attributes |
|
foreach ($this->attributes as $attribute) { |
|
$found = array_search($attribute, $rulesSearch); |
|
|
|
if ($found === false) { |
|
// Define default ruleset |
|
$rules[] = [$attribute, "text", 0, 0]; |
|
// Name | Type | Required | Filtered |
|
// Type can be: |
|
// - text |
|
// - textarea |
|
// - checkbox |
|
// - dropdown // @Todo: store dropdown data |
|
} |
|
} |
|
|
|
return $rules; |
|
} |
|
|
|
// Placeholder for generating the dropdown data |
|
public function getDropdownData(string $type): array |
|
{ |
|
return []; |
|
} |
|
|
|
public function getSort(): string |
|
{ |
|
return is_array($this->sort) |
|
? '`' . implode('`, `', $this->sort) . '`' |
|
: '`' . $this->sort . '`'; |
|
} |
|
|
|
//-------------------------------------// |
|
|
|
public static function select(string $select = '*', string $filter = '', |
|
array $parameters = [], $type = ':'): Model |
|
{ |
|
$class = get_called_class(); |
|
$model = new $class; |
|
|
|
$select = self::query("SELECT $select FROM `$model->table` $filter", |
|
$parameters, $type); |
|
if (_exists($select)) { |
|
$model->fill($select[0]); |
|
} |
|
|
|
return $model; |
|
} |
|
|
|
public static function selectAll(string $select = '*', string $filter = '', |
|
array $parameters = [], $type = ':'): ?array |
|
{ |
|
$class = get_called_class(); |
|
$model = new $class; |
|
|
|
return self::query("SELECT $select FROM `$model->table` $filter", |
|
$parameters, $type); |
|
} |
|
|
|
public static function find(int $id): Model |
|
{ |
|
$class = get_called_class(); |
|
$model = new $class; |
|
|
|
$find = self::query("SELECT * FROM `$model->table` WHERE `$model->primaryKey` = :id", [ |
|
[':id', $id], |
|
]); |
|
if (_exists($find)) { |
|
$model->fill($find[0]); |
|
} |
|
|
|
return $model; |
|
} |
|
|
|
// @Todo: add field name to query -> update Media.php md5 |
|
public static function findAll(array $id = []): ?array |
|
{ |
|
if (_exists($id)) { |
|
$id = array_values(array_unique($id)); |
|
$in = str_repeat('?, ', count($id) - 1) . '?'; |
|
array_push($id, ...$id); |
|
return self::selectAll( |
|
'*', "WHERE id IN ($in) ORDER BY FIELD(id, $in)", $id, '?'); |
|
} |
|
else { |
|
return self::selectAll(); |
|
} |
|
} |
|
|
|
public static function findOrFail(int $id): Model |
|
{ |
|
$model = self::find($id); |
|
if (!$model->exists()) { |
|
throw new \Exception('Could not find Model!'); |
|
} |
|
|
|
return $model; |
|
} |
|
|
|
public static function search(array $constraints, string $delimiter = 'AND'): Model |
|
{ |
|
$parameters = []; |
|
|
|
$filter = "WHERE "; |
|
foreach (array_keys($constraints) as $key => $constraint) { |
|
if ($key != 0) { |
|
$filter .= " $delimiter "; |
|
} |
|
|
|
$filter .= "`$constraint` = :$constraint"; |
|
$parameters[] = [":$constraint", $constraints[$constraint]]; |
|
} |
|
|
|
return self::select('*', $filter, $parameters); |
|
} |
|
|
|
public static function destroy(int $id): bool |
|
{ |
|
$model = self::find($id); |
|
if ($model->exists()) { |
|
return $model->delete(); |
|
} |
|
|
|
return false; |
|
} |
|
|
|
/** |
|
* Load all Models, optionally with a limit or pagination |
|
* |
|
* @param int $limitOrPage Treated as page if $limit is provided, limit otherwise |
|
* @param int $limit The amount to limit by |
|
* |
|
* @return array|null The found model data, or null |
|
*/ |
|
public static function all(int $limitOrPage = -1, int $limit = -1): ?array |
|
{ |
|
$class = get_called_class(); |
|
$model = new $class; |
|
|
|
$filter = ''; |
|
$parameters = []; |
|
|
|
// If the user wants to paginate |
|
if ($limitOrPage >= 1 && $limit >= 0) { |
|
// Pagination calculation |
|
$page = ($limitOrPage - 1) * $limit; |
|
|
|
$filter = 'LIMIT :page, :limit'; |
|
$parameters[] = [':page', $page, \PDO::PARAM_INT]; |
|
$parameters[] = [':limit', $limit, \PDO::PARAM_INT]; |
|
} |
|
// If the user wants an offset |
|
else if ($limitOrPage >= 0) { |
|
$filter = 'LIMIT :limit'; |
|
$parameters[] = [':limit', $limitOrPage, \PDO::PARAM_INT]; |
|
} |
|
|
|
return $model->selectAll( |
|
'*', "ORDER BY {$model->getSort()} ASC $filter", |
|
$parameters |
|
); |
|
} |
|
|
|
|
|
/** |
|
* Retreive Model, or instantiate |
|
* |
|
* Usage: |
|
* $model = \App\Model\Example::firstOrNew(['name' => 'Example name']); |
|
* |
|
* @param $search Retrieve by |
|
* @param $data Instantiate with search plus data |
|
* |
|
* @return Model The Model |
|
*/ |
|
public static function firstOrNew(array $search, array $data = []): Model |
|
{ |
|
$model = self::search($search); |
|
|
|
if (!$model->exists()) { |
|
$class = get_called_class(); |
|
$model = new $class; |
|
$model->fill($search); |
|
$model->fill($data); |
|
} |
|
|
|
return $model; |
|
} |
|
|
|
/** |
|
* Create new Model |
|
* |
|
* Usage: |
|
* $model = \App\Model\Example::create(['name' => 'Example name']); |
|
* |
|
* @param $data Create with this data |
|
* |
|
* @return Model The Model |
|
*/ |
|
public static function create(array $data): Model |
|
{ |
|
$class = get_called_class(); |
|
$model = new $class; |
|
$model->fill($data); |
|
$model->save(); |
|
return $model; |
|
} |
|
|
|
/** |
|
* Retreive Model, create if it doesn't exist |
|
* |
|
* Usage: |
|
* $model = \App\Model\AddressModel::firstOrCreate( |
|
* ['zip_code' => '1234AB', 'house_number' => 3], |
|
* ['street' => 'Example lane']); |
|
* |
|
* @param $search Retrieve by |
|
* @param $data Data used for creation |
|
* |
|
* @return Model The Model |
|
*/ |
|
public static function firstOrCreate(array $search, array $data = []): Model |
|
{ |
|
$model = self::firstOrNew($search, $data); |
|
|
|
if (!$model->exists()) { |
|
$model->save(); |
|
} |
|
|
|
return $model; |
|
} |
|
|
|
/** |
|
* Update Model, create if it doesn't exist |
|
* |
|
* Usage: |
|
* $model = \App\Model\FlightModel::updateOrCreate( |
|
* ['departure' => 'Oakland', 'desination' => 'San Diego'], |
|
* ['price' => 99]); |
|
* |
|
* @param $search Retrieve by |
|
* @param $data Data used for creation |
|
* |
|
* @return Model The Model |
|
*/ |
|
public static function updateOrCreate(array $search, array $data): Model |
|
{ |
|
$model = self::firstOrNew($search, $data); |
|
$model->fill($data); |
|
$model->save(); |
|
|
|
return $model; |
|
} |
|
|
|
} |
|
|
|
// @Todo |
|
// - Generate rules from database table |
|
// - Make count work without 'this' context
|
|
|