mirror of
https://github.com/codeigniter4/CodeIgniter4.git
synced 2025-02-20 11:44:28 +08:00
950 lines
30 KiB
PHP
950 lines
30 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* This file is part of 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 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\Database\Query;
|
|
use CodeIgniter\Entity\Entity;
|
|
use CodeIgniter\Exceptions\BadMethodCallException;
|
|
use CodeIgniter\Exceptions\ModelException;
|
|
use CodeIgniter\Validation\ValidationInterface;
|
|
use Config\Database;
|
|
use Config\Feature;
|
|
use ReflectionException;
|
|
use stdClass;
|
|
|
|
/**
|
|
* 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-read BaseConnection $db
|
|
*
|
|
* @method $this groupBy($by, ?bool $escape = null)
|
|
* @method $this groupEnd()
|
|
* @method $this groupStart()
|
|
* @method $this having($key, $value = null, ?bool $escape = null)
|
|
* @method $this havingGroupEnd()
|
|
* @method $this havingGroupStart()
|
|
* @method $this havingIn(?string $key = null, $values = null, ?bool $escape = null)
|
|
* @method $this havingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
|
|
* @method $this havingNotIn(?string $key = null, $values = null, ?bool $escape = null)
|
|
* @method $this join(string $table, string $cond, string $type = '', ?bool $escape = null)
|
|
* @method $this like($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
|
|
* @method $this limit(?int $value = null, ?int $offset = 0)
|
|
* @method $this notGroupStart()
|
|
* @method $this notHavingGroupStart()
|
|
* @method $this notHavingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
|
|
* @method $this notLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
|
|
* @method $this offset(int $offset)
|
|
* @method $this orderBy(string $orderBy, string $direction = '', ?bool $escape = null)
|
|
* @method $this orGroupStart()
|
|
* @method $this orHaving($key, $value = null, ?bool $escape = null)
|
|
* @method $this orHavingGroupStart()
|
|
* @method $this orHavingIn(?string $key = null, $values = null, ?bool $escape = null)
|
|
* @method $this orHavingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
|
|
* @method $this orHavingNotIn(?string $key = null, $values = null, ?bool $escape = null)
|
|
* @method $this orLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
|
|
* @method $this orNotGroupStart()
|
|
* @method $this orNotHavingGroupStart()
|
|
* @method $this orNotHavingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
|
|
* @method $this orNotLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
|
|
* @method $this orWhere($key, $value = null, ?bool $escape = null)
|
|
* @method $this orWhereIn(?string $key = null, $values = null, ?bool $escape = null)
|
|
* @method $this orWhereNotIn(?string $key = null, $values = null, ?bool $escape = null)
|
|
* @method $this select($select = '*', ?bool $escape = null)
|
|
* @method $this selectAvg(string $select = '', string $alias = '')
|
|
* @method $this selectCount(string $select = '', string $alias = '')
|
|
* @method $this selectMax(string $select = '', string $alias = '')
|
|
* @method $this selectMin(string $select = '', string $alias = '')
|
|
* @method $this selectSum(string $select = '', string $alias = '')
|
|
* @method $this when($condition, callable $callback, ?callable $defaultCallback = null)
|
|
* @method $this whenNot($condition, callable $callback, ?callable $defaultCallback = null)
|
|
* @method $this where($key, $value = null, ?bool $escape = null)
|
|
* @method $this whereIn(?string $key = null, $values = null, ?bool $escape = null)
|
|
* @method $this whereNotIn(?string $key = null, $values = null, ?bool $escape = null)
|
|
*
|
|
* @phpstan-import-type row_array from BaseModel
|
|
*/
|
|
class Model extends BaseModel
|
|
{
|
|
/**
|
|
* 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 bool
|
|
*/
|
|
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{escape: array, data: array}|array{}
|
|
* @phpstan-var array{escape: array<int|string, bool|null>, data: row_array}|array{}
|
|
*/
|
|
protected $tempData = [];
|
|
|
|
/**
|
|
* Escape array that maps usage of escape
|
|
* flag for every parameter.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $escape = [];
|
|
|
|
/**
|
|
* Builder method names that should not be used in the Model.
|
|
*
|
|
* @var list<string> method name
|
|
*/
|
|
private array $builderMethodsNotAvailable = [
|
|
'getCompiledInsert',
|
|
'getCompiledSelect',
|
|
'getCompiledUpdate',
|
|
];
|
|
|
|
public function __construct(?ConnectionInterface $db = null, ?ValidationInterface $validation = null)
|
|
{
|
|
/**
|
|
* @var BaseConnection|null $db
|
|
*/
|
|
$db ??= Database::connect($this->DBGroup);
|
|
|
|
$this->db = $db;
|
|
|
|
parent::__construct($validation);
|
|
}
|
|
|
|
/**
|
|
* Specify the table associated with a model
|
|
*
|
|
* @param string $table Table
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setTable(string $table)
|
|
{
|
|
$this->table = $table;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Fetches the row(s) of database from $this->table with a primary key
|
|
* matching $id.
|
|
* This method works only with dbCalls.
|
|
*
|
|
* @param bool $singleton Single or multiple results
|
|
* @param array|int|string|null $id One primary key or an array of primary keys
|
|
*
|
|
* @return array|object|null The resulting row of data, or null.
|
|
* @phpstan-return ($singleton is true ? row_array|null|object : list<row_array|object>)
|
|
*/
|
|
protected function doFind(bool $singleton, $id = null)
|
|
{
|
|
$builder = $this->builder();
|
|
|
|
$useCast = $this->useCasts();
|
|
if ($useCast) {
|
|
$returnType = $this->tempReturnType;
|
|
$this->asArray();
|
|
}
|
|
|
|
if ($this->tempUseSoftDeletes) {
|
|
$builder->where($this->table . '.' . $this->deletedField, null);
|
|
}
|
|
|
|
$row = null;
|
|
$rows = [];
|
|
|
|
if (is_array($id)) {
|
|
$rows = $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 {
|
|
$rows = $builder->get()->getResult($this->tempReturnType);
|
|
}
|
|
|
|
if ($useCast) {
|
|
$this->tempReturnType = $returnType;
|
|
|
|
if ($singleton) {
|
|
if ($row === null) {
|
|
return null;
|
|
}
|
|
|
|
return $this->convertToReturnType($row, $returnType);
|
|
}
|
|
|
|
foreach ($rows as $i => $row) {
|
|
$rows[$i] = $this->convertToReturnType($row, $returnType);
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
if ($singleton) {
|
|
return $row;
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/**
|
|
* Fetches the column of database from $this->table.
|
|
* This method works only with dbCalls.
|
|
*
|
|
* @param string $columnName Column Name
|
|
*
|
|
* @return array|null The resulting row of data, or null if no data found.
|
|
* @phpstan-return list<row_array>|null
|
|
*/
|
|
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 method works only with dbCalls.
|
|
*
|
|
* @param int|null $limit Limit
|
|
* @param int $offset Offset
|
|
*
|
|
* @return array
|
|
* @phpstan-return list<row_array|object>
|
|
*/
|
|
protected function doFindAll(?int $limit = null, int $offset = 0)
|
|
{
|
|
$limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
|
|
if ($limitZeroAsAll) {
|
|
$limit ??= 0;
|
|
}
|
|
|
|
$builder = $this->builder();
|
|
|
|
$useCast = $this->useCasts();
|
|
if ($useCast) {
|
|
$returnType = $this->tempReturnType;
|
|
$this->asArray();
|
|
}
|
|
|
|
if ($this->tempUseSoftDeletes) {
|
|
$builder->where($this->table . '.' . $this->deletedField, null);
|
|
}
|
|
|
|
$results = $builder->limit($limit, $offset)
|
|
->get()
|
|
->getResult($this->tempReturnType);
|
|
|
|
if ($useCast) {
|
|
foreach ($results as $i => $row) {
|
|
$results[$i] = $this->convertToReturnType($row, $returnType);
|
|
}
|
|
|
|
$this->tempReturnType = $returnType;
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Returns the first row of the result set. Will take any previous
|
|
* Query Builder calls into account when determining the result set.
|
|
* This method works only with dbCalls.
|
|
*
|
|
* @return array|object|null
|
|
* @phpstan-return row_array|object|null
|
|
*/
|
|
protected function doFirst()
|
|
{
|
|
$builder = $this->builder();
|
|
|
|
$useCast = $this->useCasts();
|
|
if ($useCast) {
|
|
$returnType = $this->tempReturnType;
|
|
$this->asArray();
|
|
}
|
|
|
|
if ($this->tempUseSoftDeletes) {
|
|
$builder->where($this->table . '.' . $this->deletedField, null);
|
|
} elseif ($this->useSoftDeletes && ($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 !== [] && ($builder->QBOrderBy === []) && $this->primaryKey !== '') {
|
|
$builder->orderBy($this->table . '.' . $this->primaryKey, 'asc');
|
|
}
|
|
|
|
$row = $builder->limit(1, 0)->get()->getFirstRow($this->tempReturnType);
|
|
|
|
if ($useCast && $row !== null) {
|
|
$row = $this->convertToReturnType($row, $returnType);
|
|
|
|
$this->tempReturnType = $returnType;
|
|
}
|
|
|
|
return $row;
|
|
}
|
|
|
|
/**
|
|
* Inserts data into the current table.
|
|
* This method works only with dbCalls.
|
|
*
|
|
* @param array $row Row data
|
|
* @phpstan-param row_array $row
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function doInsert(array $row)
|
|
{
|
|
$escape = $this->escape;
|
|
$this->escape = [];
|
|
|
|
// Require non-empty primaryKey when
|
|
// not using auto-increment feature
|
|
if (! $this->useAutoIncrement && ! isset($row[$this->primaryKey])) {
|
|
throw DataException::forEmptyPrimaryKey('insert');
|
|
}
|
|
|
|
$builder = $this->builder();
|
|
|
|
// Must use the set() method to ensure to set the correct escape flag
|
|
foreach ($row as $key => $val) {
|
|
$builder->set($key, $val, $escape[$key] ?? null);
|
|
}
|
|
|
|
if ($this->allowEmptyInserts && $row === []) {
|
|
$table = $this->db->protectIdentifiers($this->table, true, null, false);
|
|
if ($this->db->getPlatform() === 'MySQLi') {
|
|
$sql = 'INSERT INTO ' . $table . ' VALUES ()';
|
|
} elseif ($this->db->getPlatform() === 'OCI8') {
|
|
$allFields = $this->db->protectIdentifiers(
|
|
array_map(
|
|
static fn ($row) => $row->name,
|
|
$this->db->getFieldData($this->table),
|
|
),
|
|
false,
|
|
true,
|
|
);
|
|
|
|
$sql = sprintf(
|
|
'INSERT INTO %s (%s) VALUES (%s)',
|
|
$table,
|
|
implode(',', $allFields),
|
|
substr(str_repeat(',DEFAULT', count($allFields)), 1),
|
|
);
|
|
} else {
|
|
$sql = 'INSERT INTO ' . $table . ' DEFAULT VALUES';
|
|
}
|
|
|
|
$result = $this->db->query($sql);
|
|
} else {
|
|
$result = $builder->insert();
|
|
}
|
|
|
|
// If insertion succeeded then save the insert ID
|
|
if ($result) {
|
|
$this->insertID = ! $this->useAutoIncrement ? $row[$this->primaryKey] : $this->db->insertID();
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Compiles batch insert strings and runs the queries, validating each row prior.
|
|
* This method works only with dbCalls.
|
|
*
|
|
* @param array|null $set An associative array of insert values
|
|
* @param bool|null $escape Whether to escape values
|
|
* @param int $batchSize The size of the batch to run
|
|
* @param bool $testing True means only number of records is returned, false will execute the query
|
|
*
|
|
* @return bool|int 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 && ! isset($row[$this->primaryKey])) {
|
|
throw DataException::forEmptyPrimaryKey('insertBatch');
|
|
}
|
|
}
|
|
}
|
|
|
|
return $this->builder()->testMode($testing)->insertBatch($set, $escape, $batchSize);
|
|
}
|
|
|
|
/**
|
|
* Updates a single record in $this->table.
|
|
* This method works only with dbCalls.
|
|
*
|
|
* @param array|int|string|null $id
|
|
* @param array|null $row Row data
|
|
* @phpstan-param row_array|null $row
|
|
*/
|
|
protected function doUpdate($id = null, $row = null): bool
|
|
{
|
|
$escape = $this->escape;
|
|
$this->escape = [];
|
|
|
|
$builder = $this->builder();
|
|
|
|
if (! in_array($id, [null, '', 0, '0', []], true)) {
|
|
$builder = $builder->whereIn($this->table . '.' . $this->primaryKey, $id);
|
|
}
|
|
|
|
// Must use the set() method to ensure to set the correct escape flag
|
|
foreach ($row as $key => $val) {
|
|
$builder->set($key, $val, $escape[$key] ?? null);
|
|
}
|
|
|
|
if ($builder->getCompiledQBWhere() === []) {
|
|
throw new DatabaseException(
|
|
'Updates are not allowed unless they contain a "where" or "like" clause.',
|
|
);
|
|
}
|
|
|
|
return $builder->update();
|
|
}
|
|
|
|
/**
|
|
* Compiles an update string and runs the query
|
|
* This method works only with dbCalls.
|
|
*
|
|
* @param array|null $set An associative array of update values
|
|
* @param string|null $index The where key
|
|
* @param int $batchSize The size of the batch to run
|
|
* @param bool $returnSQL True means SQL is returned, false will execute the query
|
|
*
|
|
* @return false|int|list<string> Number of rows affected or FALSE on failure, SQL array when testMode
|
|
*
|
|
* @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 method works only with dbCalls.
|
|
*
|
|
* @param array|int|string|null $id The rows primary key(s)
|
|
* @param bool $purge Allows overriding the soft deletes setting.
|
|
*
|
|
* @return bool|string SQL string when testMode
|
|
*
|
|
* @throws DatabaseException
|
|
*/
|
|
protected function doDelete($id = null, bool $purge = false)
|
|
{
|
|
$set = [];
|
|
$builder = $this->builder();
|
|
|
|
if (! in_array($id, [null, '', 0, '0', []], true)) {
|
|
$builder = $builder->whereIn($this->primaryKey, $id);
|
|
}
|
|
|
|
if ($this->useSoftDeletes && ! $purge) {
|
|
if ($builder->getCompiledQBWhere() === []) {
|
|
throw new DatabaseException(
|
|
'Deletes are not allowed unless they contain a "where" or "like" clause.',
|
|
);
|
|
}
|
|
|
|
$builder->where($this->deletedField);
|
|
|
|
$set[$this->deletedField] = $this->setDate();
|
|
|
|
if ($this->useTimestamps && $this->updatedField !== '') {
|
|
$set[$this->updatedField] = $this->setDate();
|
|
}
|
|
|
|
return $builder->update($set);
|
|
}
|
|
|
|
return $builder->delete();
|
|
}
|
|
|
|
/**
|
|
* Permanently deletes all rows that have been marked as deleted
|
|
* through soft deletes (deleted = 1)
|
|
* This method works only with dbCalls.
|
|
*
|
|
* @return bool|string Returns a SQL string if in test mode.
|
|
*/
|
|
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 method 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 method works only with dbCalls.
|
|
*
|
|
* @param array|null $row Data
|
|
* @phpstan-param row_array|null $row
|
|
* @param bool $returnSQL Set to true to return Query String
|
|
*
|
|
* @return BaseResult|false|Query|string
|
|
*/
|
|
protected function doReplace(?array $row = null, bool $returnSQL = false)
|
|
{
|
|
return $this->builder()->testMode($returnSQL)->replace($row);
|
|
}
|
|
|
|
/**
|
|
* Grabs the last error(s) that occurred from the Database connection.
|
|
* The return array should be in the following format:
|
|
* ['source' => 'message']
|
|
* This method works only with dbCalls.
|
|
*
|
|
* @return array<string, string>
|
|
*/
|
|
protected function doErrors()
|
|
{
|
|
// $error is always ['code' => string|int, 'message' => string]
|
|
$error = $this->db->error();
|
|
|
|
if ((int) $error['code'] === 0) {
|
|
return [];
|
|
}
|
|
|
|
return [$this->db::class => $error['message']];
|
|
}
|
|
|
|
/**
|
|
* Returns the id value for the data array or object
|
|
*
|
|
* @param array|object $row Row data
|
|
* @phpstan-param row_array|object $row
|
|
*
|
|
* @return array|int|string|null
|
|
*/
|
|
public function getIdValue($row)
|
|
{
|
|
if (is_object($row)) {
|
|
// Get the raw or mapped primary key value of the Entity.
|
|
if ($row instanceof Entity && $row->{$this->primaryKey} !== null) {
|
|
$cast = $row->cast();
|
|
|
|
// Disable Entity casting, because raw primary key value is needed for database.
|
|
$row->cast(false);
|
|
|
|
$primaryKey = $row->{$this->primaryKey};
|
|
|
|
// Restore Entity casting setting.
|
|
$row->cast($cast);
|
|
|
|
return $primaryKey;
|
|
}
|
|
|
|
if (! $row instanceof Entity && isset($row->{$this->primaryKey})) {
|
|
return $row->{$this->primaryKey};
|
|
}
|
|
}
|
|
|
|
if (is_array($row) && isset($row[$this->primaryKey])) {
|
|
return $row[$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 method works only with dbCalls.
|
|
*/
|
|
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 ($rows === []) {
|
|
continue;
|
|
}
|
|
|
|
foreach ($rows as $row) {
|
|
if ($userFunc($row) === false) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Override countAllResults to account for soft deleted accounts.
|
|
*
|
|
* @return int|string
|
|
*/
|
|
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
|
|
// dependent 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);
|
|
}
|
|
|
|
/**
|
|
* Provides a shared instance of the Query Builder.
|
|
*
|
|
* @param non-empty-string|null $table
|
|
*
|
|
* @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 ((string) $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 ($this->primaryKey === '') {
|
|
throw ModelException::forNoPrimaryKey(static::class);
|
|
}
|
|
|
|
$table = ((string) $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 array|object|string $key Field name, or an array of field/value pairs, or an object
|
|
* @param bool|float|int|object|string|null $value Field value, if $key is a single field
|
|
* @param bool|null $escape Whether to escape values
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function set($key, $value = '', ?bool $escape = null)
|
|
{
|
|
if (is_object($key)) {
|
|
$key = $key instanceof stdClass ? (array) $key : $this->objectToArray($key);
|
|
}
|
|
|
|
$data = is_array($key) ? $key : [$key => $value];
|
|
|
|
foreach (array_keys($data) as $k) {
|
|
$this->tempData['escape'][$k] = $escape;
|
|
}
|
|
|
|
$this->tempData['data'] = array_merge($this->tempData['data'] ?? [], $data);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* 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 $row Data
|
|
*/
|
|
protected function shouldUpdate($row): bool
|
|
{
|
|
if (parent::shouldUpdate($row) === false) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->useAutoIncrement === true) {
|
|
return true;
|
|
}
|
|
|
|
// When useAutoIncrement feature is disabled, check
|
|
// in the database if given record already exists
|
|
return $this->where($this->primaryKey, $this->getIdValue($row))->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 $row
|
|
* @phpstan-param row_array|object|null $row
|
|
* @param bool $returnID Whether insert ID should be returned or not.
|
|
*
|
|
* @return bool|int|string
|
|
* @phpstan-return ($returnID is true ? int|string|false : bool)
|
|
*
|
|
* @throws ReflectionException
|
|
*/
|
|
public function insert($row = null, bool $returnID = true)
|
|
{
|
|
if (isset($this->tempData['data'])) {
|
|
if ($row === null) {
|
|
$row = $this->tempData['data'];
|
|
} else {
|
|
$row = $this->transformDataToArray($row, 'insert');
|
|
$row = array_merge($this->tempData['data'], $row);
|
|
}
|
|
}
|
|
|
|
$this->escape = $this->tempData['escape'] ?? [];
|
|
$this->tempData = [];
|
|
|
|
return parent::insert($row, $returnID);
|
|
}
|
|
|
|
/**
|
|
* Ensures that only the fields that are allowed to be inserted are in
|
|
* the data array.
|
|
*
|
|
* @used-by insert() to protect against mass assignment vulnerabilities.
|
|
* @used-by insertBatch() to protect against mass assignment vulnerabilities.
|
|
*
|
|
* @param array $row Row data
|
|
* @phpstan-param row_array $row
|
|
*
|
|
* @throws DataException
|
|
*/
|
|
protected function doProtectFieldsForInsert(array $row): array
|
|
{
|
|
if (! $this->protectFields) {
|
|
return $row;
|
|
}
|
|
|
|
if ($this->allowedFields === []) {
|
|
throw DataException::forInvalidAllowedFields(static::class);
|
|
}
|
|
|
|
foreach (array_keys($row) as $key) {
|
|
// Do not remove the non-auto-incrementing primary key data.
|
|
if ($this->useAutoIncrement === false && $key === $this->primaryKey) {
|
|
continue;
|
|
}
|
|
|
|
if (! in_array($key, $this->allowedFields, true)) {
|
|
unset($row[$key]);
|
|
}
|
|
}
|
|
|
|
return $row;
|
|
}
|
|
|
|
/**
|
|
* Updates a single record in the database. If an object is provided,
|
|
* it will attempt to convert it into an array.
|
|
*
|
|
* @param array|int|string|null $id
|
|
* @param array|object|null $row
|
|
* @phpstan-param row_array|object|null $row
|
|
*
|
|
* @throws ReflectionException
|
|
*/
|
|
public function update($id = null, $row = null): bool
|
|
{
|
|
if (isset($this->tempData['data'])) {
|
|
if ($row === null) {
|
|
$row = $this->tempData['data'];
|
|
} else {
|
|
$row = $this->transformDataToArray($row, 'update');
|
|
$row = array_merge($this->tempData['data'], $row);
|
|
}
|
|
}
|
|
|
|
$this->escape = $this->tempData['escape'] ?? [];
|
|
$this->tempData = [];
|
|
|
|
return parent::update($id, $row);
|
|
}
|
|
|
|
/**
|
|
* Takes a class and returns an array of its public and protected
|
|
* properties as an array with raw values.
|
|
*
|
|
* @param object $object Object
|
|
* @param bool $recursive If true, inner entities will be cast as array as well
|
|
*
|
|
* @return array<string, mixed> Array with raw values.
|
|
*
|
|
* @throws ReflectionException
|
|
*/
|
|
protected function objectToRawArray($object, bool $onlyChanged = true, bool $recursive = false): array
|
|
{
|
|
return parent::objectToRawArray($object, $onlyChanged);
|
|
}
|
|
|
|
/**
|
|
* Provides/instantiates the builder/db connection and model's table/primary key names and return type.
|
|
*
|
|
* @param string $name Name
|
|
*
|
|
* @return array|BaseBuilder|bool|float|int|object|string|null
|
|
*/
|
|
public function __get(string $name)
|
|
{
|
|
if (parent::__isset($name)) {
|
|
return parent::__get($name);
|
|
}
|
|
|
|
return $this->builder()->{$name} ?? null;
|
|
}
|
|
|
|
/**
|
|
* Checks for the existence of properties across this model, builder, and db connection.
|
|
*
|
|
* @param string $name Name
|
|
*/
|
|
public function __isset(string $name): bool
|
|
{
|
|
if (parent::__isset($name)) {
|
|
return true;
|
|
}
|
|
|
|
return isset($this->builder()->{$name});
|
|
}
|
|
|
|
/**
|
|
* Provides direct access to method in the builder (if available)
|
|
* and the database connection.
|
|
*
|
|
* @return $this|array|BaseBuilder|bool|float|int|object|string|null
|
|
*/
|
|
public function __call(string $name, array $params)
|
|
{
|
|
$builder = $this->builder();
|
|
$result = null;
|
|
|
|
if (method_exists($this->db, $name)) {
|
|
$result = $this->db->{$name}(...$params);
|
|
} elseif (method_exists($builder, $name)) {
|
|
$this->checkBuilderMethod($name);
|
|
|
|
$result = $builder->{$name}(...$params);
|
|
} else {
|
|
throw new BadMethodCallException('Call to undefined method ' . static::class . '::' . $name);
|
|
}
|
|
|
|
if ($result instanceof BaseBuilder) {
|
|
return $this;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Checks the Builder method name that should not be used in the Model.
|
|
*/
|
|
private function checkBuilderMethod(string $name): void
|
|
{
|
|
if (in_array($name, $this->builderMethodsNotAvailable, true)) {
|
|
throw ModelException::forMethodNotAvailable(static::class, $name . '()');
|
|
}
|
|
}
|
|
}
|