File Manager V1.5
FILE_CONTENT: Model.php
<?php
/**
* This file is part of the CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace CodeIgniter;
use BadMethodCallException;
use Closure;
use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Database\BaseResult;
use CodeIgniter\Database\ConnectionInterface;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\Exceptions\DataException;
use CodeIgniter\Exceptions\ModelException;
use CodeIgniter\I18n\Time;
use CodeIgniter\Validation\ValidationInterface;
use Config\Database;
use ReflectionClass;
use ReflectionException;
use ReflectionProperty;
/**
* Class Model
*
* The Model class extends BaseModel and provides additional
* convenient features that makes working with a SQL database
* table less painful.
*
* It will:
* - automatically connect to database
* - allow intermingling calls to the builder
* - removes the need to use Result object directly in most cases
*
* @property ConnectionInterface $db
*
* @mixin BaseBuilder
*/
class Model extends BaseModel
{
// region Properties
/**
* Name of database table
*
* @var string
*/
protected $table;
/**
* The table's primary key.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Whether primary key uses auto increment.
*
* @var boolean
*/
protected $useAutoIncrement = true;
/**
* Query Builder object
*
* @var BaseBuilder|null
*/
protected $builder;
/**
* Holds information passed in via 'set'
* so that we can capture it (not the builder)
* and ensure it gets validated first.
*
* @var array
*/
protected $tempData = [];
/**
* Escape array that maps usage of escape
* flag for every parameter.
*
* @var array
*/
protected $escape = [];
// endregion
// region Constructor
/**
* Model constructor.
*
* @param ConnectionInterface|null $db DB Connection
* @param ValidationInterface|null $validation Validation
*/
public function __construct(ConnectionInterface &$db = null, ValidationInterface $validation = null)
{
parent::__construct($validation);
if (is_null($db))
{
$this->db = Database::connect($this->DBGroup);
}
else
{
$this->db = &$db;
}
}
// endregion
// region Setters
/**
* Specify the table associated with a model
*
* @param string $table Table
*
* @return $this
*/
public function setTable(string $table)
{
$this->table = $table;
return $this;
}
// endregion
// region Database Methods
/**
* Fetches the row of database from $this->table with a primary key
* matching $id. This methods works only with dbCalls
* This methods works only with dbCalls
*
* @param boolean $singleton Single or multiple results
* @param array|integer|string|null $id One primary key or an array of primary keys
*
* @return array|object|null The resulting row of data, or null.
*/
protected function doFind(bool $singleton, $id = null)
{
$builder = $this->builder();
if ($this->tempUseSoftDeletes)
{
$builder->where($this->table . '.' . $this->deletedField, null);
}
if (is_array($id))
{
$row = $builder->whereIn($this->table . '.' . $this->primaryKey, $id)
->get()
->getResult($this->tempReturnType);
}
elseif ($singleton)
{
$row = $builder->where($this->table . '.' . $this->primaryKey, $id)
->get()
->getFirstRow($this->tempReturnType);
}
else
{
$row = $builder->get()->getResult($this->tempReturnType);
}
return $row;
}
/**
* Fetches the column of database from $this->table
* This methods works only with dbCalls
*
* @param string $columnName Column Name
*
* @return array|null The resulting row of data, or null if no data found.
*/
protected function doFindColumn(string $columnName)
{
return $this->select($columnName)->asArray()->find();
}
/**
* Works with the current Query Builder instance to return
* all results, while optionally limiting them.
* This methods works only with dbCalls
*
* @param integer $limit Limit
* @param integer $offset Offset
*
* @return array
*/
protected function doFindAll(int $limit = 0, int $offset = 0)
{
$builder = $this->builder();
if ($this->tempUseSoftDeletes)
{
$builder->where($this->table . '.' . $this->deletedField, null);
}
return $builder->limit($limit, $offset)
->get()
->getResult($this->tempReturnType);
}
/**
* Returns the first row of the result set. Will take any previous
* Query Builder calls into account when determining the result set.
* This methods works only with dbCalls
*
* @return array|object|null
*/
protected function doFirst()
{
$builder = $this->builder();
if ($this->tempUseSoftDeletes)
{
$builder->where($this->table . '.' . $this->deletedField, null);
}
else
{
if ($this->useSoftDeletes && empty($builder->QBGroupBy) && $this->primaryKey)
{
$builder->groupBy($this->table . '.' . $this->primaryKey);
}
}
// Some databases, like PostgreSQL, need order
// information to consistently return correct results.
if ($builder->QBGroupBy && empty($builder->QBOrderBy) && $this->primaryKey)
{
$builder->orderBy($this->table . '.' . $this->primaryKey, 'asc');
}
return $builder->limit(1, 0)->get()->getFirstRow($this->tempReturnType);
}
/**
* Inserts data into the current table.
* This methods works only with dbCalls
*
* @param array $data Data
*
* @return BaseResult|integer|string|false
*/
protected function doInsert(array $data)
{
$escape = $this->escape;
$this->escape = [];
// Require non empty primaryKey when
// not using auto-increment feature
if (! $this->useAutoIncrement && empty($data[$this->primaryKey]))
{
throw DataException::forEmptyPrimaryKey('insert');
}
$builder = $this->builder();
// Must use the set() method to ensure to set the correct escape flag
foreach ($data as $key => $val)
{
$builder->set($key, $val, $escape[$key] ?? null);
}
$result = $builder->insert();
// If insertion succeeded then save the insert ID
if ($result->resultID)
{
if (! $this->useAutoIncrement)
{
$this->insertID = $data[$this->primaryKey];
}
else
{
// @phpstan-ignore-next-line
$this->insertID = $this->db->insertID();
}
}
return $result;
}
/**
* Compiles batch insert strings and runs the queries, validating each row prior.
* This methods works only with dbCalls
*
* @param array|null $set An associative array of insert values
* @param boolean|null $escape Whether to escape values and identifiers
* @param integer $batchSize The size of the batch to run
* @param boolean $testing True means only number of records is returned, false will execute the query
*
* @return integer|boolean Number of rows inserted or FALSE on failure
*/
protected function doInsertBatch(?array $set = null, ?bool $escape = null, int $batchSize = 100, bool $testing = false)
{
if (is_array($set))
{
foreach ($set as $row)
{
// Require non empty primaryKey when
// not using auto-increment feature
if (! $this->useAutoIncrement && empty($row[$this->primaryKey]))
{
throw DataException::forEmptyPrimaryKey('insertBatch');
}
}
}
return $this->builder()->testMode($testing)->insertBatch($set, $escape, $batchSize);
}
/**
* Updates a single record in $this->table.
* This methods works only with dbCalls
*
* @param integer|array|string|null $id ID
* @param array|null $data Data
*
* @return boolean
*/
protected function doUpdate($id = null, $data = null): bool
{
$escape = $this->escape;
$this->escape = [];
$builder = $this->builder();
if ($id)
{
$builder = $builder->whereIn($this->table . '.' . $this->primaryKey, $id);
}
// Must use the set() method to ensure to set the correct escape flag
foreach ($data as $key => $val)
{
$builder->set($key, $val, $escape[$key] ?? null);
}
return $builder->update();
}
/**
* Compiles an update string and runs the query
* This methods works only with dbCalls
*
* @param array|null $set An associative array of update values
* @param string|null $index The where key
* @param integer $batchSize The size of the batch to run
* @param boolean $returnSQL True means SQL is returned, false will execute the query
*
* @return mixed Number of rows affected or FALSE on failure
*
* @throws DatabaseException
*/
protected function doUpdateBatch(array $set = null, string $index = null, int $batchSize = 100, bool $returnSQL = false)
{
return $this->builder()->testMode($returnSQL)->updateBatch($set, $index, $batchSize);
}
/**
* Deletes a single record from $this->table where $id matches
* the table's primaryKey
* This methods works only with dbCalls
*
* @param integer|string|array|null $id The rows primary key(s)
* @param boolean $purge Allows overriding the soft deletes setting.
*
* @return BaseResult|boolean
*
* @throws DatabaseException
*/
protected function doDelete($id = null, bool $purge = false)
{
$builder = $this->builder();
if ($id)
{
$builder = $builder->whereIn($this->primaryKey, $id);
}
if ($this->useSoftDeletes && ! $purge)
{
if (empty($builder->getCompiledQBWhere()))
{
if (CI_DEBUG)
{
throw new DatabaseException(
'Deletes are not allowed unless they contain a "where" or "like" clause.'
);
}
// @codeCoverageIgnoreStart
return false;
// @codeCoverageIgnoreEnd
}
$set[$this->deletedField] = $this->setDate();
if ($this->useTimestamps && $this->updatedField)
{
$set[$this->updatedField] = $this->setDate();
}
$result = $builder->update($set);
}
else
{
$result = $builder->delete();
}
return $result;
}
/**
* Permanently deletes all rows that have been marked as deleted
* through soft deletes (deleted = 1)
* This methods works only with dbCalls
*
* @return boolean|mixed
*/
protected function doPurgeDeleted()
{
return $this->builder()
->where($this->table . '.' . $this->deletedField . ' IS NOT NULL')
->delete();
}
/**
* Works with the find* methods to return only the rows that
* have been deleted.
* This methods works only with dbCalls
*
* @return void
*/
protected function doOnlyDeleted()
{
$this->builder()->where($this->table . '.' . $this->deletedField . ' IS NOT NULL');
}
/**
* Compiles a replace into string and runs the query
* This methods works only with dbCalls
*
* @param array|null $data Data
* @param boolean $returnSQL Set to true to return Query String
*
* @return mixed
*/
protected function doReplace(array $data = null, bool $returnSQL = false)
{
return $this->builder()->testMode($returnSQL)->replace($data);
}
/**
* Grabs the last error(s) that occurred from the Database connection.
* This methods works only with dbCalls
*
* @return array|null
*/
protected function doErrors()
{
return $this->db->error();
}
/**
* Returns the id value for the data array or object
*
* @param array|object $data Data
*
* @return integer|array|string|null
*/
protected function idValue($data)
{
if (is_object($data) && isset($data->{$this->primaryKey}))
{
return $data->{$this->primaryKey};
}
if (is_array($data) && ! empty($data[$this->primaryKey]))
{
return $data[$this->primaryKey];
}
return null;
}
/**
* Loops over records in batches, allowing you to operate on them.
* Works with $this->builder to get the Compiled select to
* determine the rows to operate on.
* This methods works only with dbCalls
*
* @param integer $size Size
* @param Closure $userFunc Callback Function
*
* @return void
*
* @throws DataException
*/
public function chunk(int $size, Closure $userFunc)
{
$total = $this->builder()->countAllResults(false);
$offset = 0;
while ($offset <= $total)
{
$builder = clone $this->builder();
$rows = $builder->get($size, $offset);
if (! $rows)
{
throw DataException::forEmptyDataset('chunk');
}
$rows = $rows->getResult($this->tempReturnType);
$offset += $size;
if (empty($rows))
{
continue;
}
foreach ($rows as $row)
{
if ($userFunc($row) === false)
{
return;
}
}
}
}
/**
* Override countAllResults to account for soft deleted accounts.
*
* @param boolean $reset Reset
* @param boolean $test Test
*
* @return mixed
*/
public function countAllResults(bool $reset = true, bool $test = false)
{
if ($this->tempUseSoftDeletes)
{
$this->builder()->where($this->table . '.' . $this->deletedField, null);
}
// When $reset === false, the $tempUseSoftDeletes will be
// dependant on $useSoftDeletes value because we don't
// want to add the same "where" condition for the second time
$this->tempUseSoftDeletes = $reset
? $this->useSoftDeletes
: ($this->useSoftDeletes ? false : $this->useSoftDeletes);
return $this->builder()->testMode($test)->countAllResults($reset);
}
// endregion
// region Builder
/**
* Provides a shared instance of the Query Builder.
*
* @param string|null $table Table name
*
* @return BaseBuilder
* @throws ModelException
*/
public function builder(?string $table = null)
{
// Check for an existing Builder
if ($this->builder instanceof BaseBuilder)
{
// Make sure the requested table matches the builder
if ($table && $this->builder->getTable() !== $table)
{
return $this->db->table($table);
}
return $this->builder;
}
// We're going to force a primary key to exist
// so we don't have overly convoluted code,
// and future features are likely to require them.
if (empty($this->primaryKey))
{
throw ModelException::forNoPrimaryKey(static::class);
}
$table = empty($table) ? $this->table : $table;
// Ensure we have a good db connection
if (! $this->db instanceof BaseConnection)
{
$this->db = Database::connect($this->DBGroup);
}
$builder = $this->db->table($table);
// Only consider it "shared" if the table is correct
if ($table === $this->table)
{
$this->builder = $builder;
}
return $builder;
}
/**
* Captures the builder's set() method so that we can validate the
* data here. This allows it to be used with any of the other
* builder methods and still get validated data, like replace.
*
* @param mixed $key Field name, or an array of field/value pairs
* @param string|null $value Field value, if $key is a single field
* @param boolean|null $escape Whether to escape values and identifiers
*
* @return $this
*/
public function set($key, ?string $value = '', ?bool $escape = null)
{
$data = is_array($key) ? $key : [$key => $value];
foreach ($data as $k => $v)
{
$this->tempData['escape'][$k] = $escape;
}
$this->tempData['data'] = array_merge($this->tempData['data'] ?? [], $data);
return $this;
}
// endregion
// region Overrides
// region CRUD & Finders
/**
* This method is called on save to determine if entry have to be updated
* If this method return false insert operation will be executed
*
* @param array|object $data Data
*
* @return boolean
*/
protected function shouldUpdate($data) : bool
{
// When useAutoIncrement feature is disabled check
// in the database if given record already exists
return parent::shouldUpdate($data) &&
($this->useAutoIncrement
? true
: $this->where($this->primaryKey, $this->idValue($data))->countAllResults() === 1
);
}
/**
* Inserts data into the database. If an object is provided,
* it will attempt to convert it to an array.
*
* @param array|object|null $data Data
* @param boolean $returnID Whether insert ID should be returned or not.
*
* @return BaseResult|object|integer|string|false
*
* @throws ReflectionException
*/
public function insert($data = null, bool $returnID = true)
{
if (! empty($this->tempData['data']))
{
if (empty($data))
{
$data = $this->tempData['data'] ?? null;
}
else
{
$data = $this->transformDataToArray($data, 'insert');
$data = array_merge($this->tempData['data'], $data);
}
}
$this->escape = $this->tempData['escape'] ?? [];
$this->tempData = [];
return parent::insert($data, $returnID);
}
/**
* Updates a single record in the database. If an object is provided,
* it will attempt to convert it into an array.
*
* @param integer|array|string|null $id ID
* @param array|object|null $data Data
*
* @return boolean
*
* @throws ReflectionException
*/
public function update($id = null, $data = null): bool
{
if (! empty($this->tempData['data']))
{
if (empty($data))
{
$data = $this->tempData['data'] ?? null;
}
else
{
$data = $this->transformDataToArray($data, 'update');
$data = array_merge($this->tempData['data'], $data);
}
}
$this->escape = $this->tempData['escape'] ?? [];
$this->tempData = [];
return parent::update($id, $data);
}
// endregion
// region Utility
/**
* Takes a class an returns an array of it's public and protected
* properties as an array with raw values.
*
* @param string|object $data Data
* @param boolean $onlyChanged Only Changed Property
* @param boolean $recursive If true, inner entities will be casted as array as well
*
* @return array|null Array
*
* @throws ReflectionException
*/
protected function objectToRawArray($data, bool $onlyChanged = true, bool $recursive = false): ?array
{
$properties = parent::objectToRawArray($data, $onlyChanged);
if (method_exists($data, 'toRawArray'))
{
// Always grab the primary key otherwise updates will fail.
if (! empty($properties) && ! empty($this->primaryKey) && ! in_array($this->primaryKey, $properties, true)
&& ! empty($data->{$this->primaryKey}))
{
$properties[$this->primaryKey] = $data->{$this->primaryKey};
}
}
return $properties;
}
// endregion
// region Magic
/**
* Provides/instantiates the builder/db connection and model's table/primary key names and return type.
*
* @param string $name Name
*
* @return mixed
*/
public function __get(string $name)
{
if (parent::__isset($name))
{
return parent::__get($name);
}
if (isset($this->builder()->$name))
{
return $this->builder()->$name;
}
return null;
}
/**
* Checks for the existence of properties across this model, builder, and db connection.
*
* @param string $name Name
*
* @return boolean
*/
public function __isset(string $name): bool
{
if (parent::__isset($name))
{
return true;
}
if (isset($this->builder()->$name))
{
return true;
}
return false;
}
/**
* Provides direct access to method in the builder (if available)
* and the database connection.
*
* @param string $name Name
* @param array $params Params
*
* @return $this|null
*/
public function __call(string $name, array $params)
{
$result = parent::__call($name, $params);
if ($result === null && method_exists($builder = $this->builder(), $name))
{
$result = $builder->{$name}(...$params);
}
if (empty($result))
{
if (! method_exists($this->builder(), $name))
{
$className = static::class;
throw new BadMethodCallException('Call to undefined method ' . $className . '::' . $name);
}
return $result;
}
if ($result instanceof BaseBuilder)
{
return $this;
}
return $result;
}
// endregion
// endregion
// region Deprecated
/**
* Takes a class an returns an array of it's public and protected
* properties as an array suitable for use in creates and updates.
*
* @param string|object $data Data
* @param string|null $primaryKey Primary Key
* @param string $dateFormat Date Format
* @param boolean $onlyChanged Only Changed
*
* @return array
*
* @throws ReflectionException
*
* @codeCoverageIgnore
*
* @deprecated since 4.1
*/
public static function classToArray($data, $primaryKey = null, string $dateFormat = 'datetime', bool $onlyChanged = true): array
{
if (method_exists($data, 'toRawArray'))
{
$properties = $data->toRawArray($onlyChanged);
// Always grab the primary key otherwise updates will fail.
if (! empty($properties) && ! empty($primaryKey) && ! in_array($primaryKey, $properties, true) && ! empty($data->{$primaryKey}))
{
$properties[$primaryKey] = $data->{$primaryKey};
}
}
else
{
$mirror = new ReflectionClass($data);
$props = $mirror->getProperties(ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED);
$properties = [];
// Loop over each property,
// saving the name/value in a new array we can return.
foreach ($props as $prop)
{
// Must make protected values accessible.
$prop->setAccessible(true);
$properties[$prop->getName()] = $prop->getValue($data);
}
}
// Convert any Time instances to appropriate $dateFormat
if ($properties)
{
foreach ($properties as $key => $value)
{
if ($value instanceof Time)
{
switch ($dateFormat)
{
case 'datetime':
$converted = $value->format('Y-m-d H:i:s');
break;
case 'date':
$converted = $value->format('Y-m-d');
break;
case 'int':
$converted = $value->getTimestamp();
break;
default:
$converted = (string) $value;
}
$properties[$key] = $converted;
}
}
}
return $properties;
}
// endregion
}
[ KEMBALI ]