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.
504 lines
11 KiB
504 lines
11 KiB
4 years ago
|
<?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;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//-------------------------------------//
|
||
|
|
||
|
protected static function query(string $query, array $parameters = [],
|
||
|
$type = ':'): ?array
|
||
|
{
|
||
|
// Example
|
||
|
// $parameters = [
|
||
|
// [':id', 1],
|
||
|
// [':number', 7, \PDO::PARAM_INT],
|
||
|
// [':string', 'A random string', \PDO::PARAM_STR],
|
||
|
// ];
|
||
|
|
||
|
// PDO::PARAM_BOOL
|
||
|
// PDO::PARAM_NULL
|
||
|
// PDO::PARAM_INT
|
||
|
// PDO::PARAM_STR
|
||
|
|
||
|
if (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 (_exists($fill, $attribute) ||
|
||
|
(isset($fill[$attribute]) && $fill[$attribute] === '0')) {
|
||
|
// Escape sequences are only interpreted with double quotes!
|
||
|
$this->{$attribute} = preg_replace('/\r\n?/', "\n", $fill[$attribute]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
// $media = MediaModel::selectAll(
|
||
|
// '*', 'ORDER BY id DESC LIMIT :offset, :limit', [
|
||
|
// [':offset', $offset, \PDO::PARAM_INT],
|
||
|
// [':limit', $limit, \PDO::PARAM_INT],
|
||
|
// ]
|
||
|
// );
|
||
|
//
|
||
|
// $contents = ContentModel::selectAll(
|
||
|
// '*', 'WHERE id IN (?, ?, ?)', [1, 2, 3], '?'
|
||
|
// );
|
||
|
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 Model data: all, 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, or create
|
||
|
* Usage: $model = \App\Model\Example::firstOrCreate(['name' => 'Example name']);
|
||
|
*
|
||
|
* @param $search Retrieve by
|
||
|
* @param $data Instantiate with search plus data
|
||
|
*
|
||
|
* @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 existing Model, or create it if doesn't exist
|
||
|
// public static function updateOrCreate(array $data, array $data): Model {
|
||
|
// // $flight = App\Flight::updateOrCreate(
|
||
|
// // ['departure' => 'Oakland', 'destination' => 'San Diego'],
|
||
|
// // ['price' => 99]
|
||
|
// // );
|
||
|
// return new Model;
|
||
|
// }
|
||
|
|
||
|
}
|
||
|
|
||
|
// @Todo
|
||
|
// - Generate rules from database table
|
||
|
// - Make count work without 'this' context
|