OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
413
app/Core/Db/DatabaseHelper.php
Normal file
413
app/Core/Db/DatabaseHelper.php
Normal file
@@ -0,0 +1,413 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\Db;
|
||||
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
|
||||
/**
|
||||
* DatabaseHelper provides cross-database compatibility for common SQL functions
|
||||
*
|
||||
* This helper abstracts database-specific SQL syntax to support MySQL, PostgreSQL, and MS SQL Server.
|
||||
* It handles functions like GROUP_CONCAT, WEEK(), date functions, and other database-specific operations.
|
||||
*/
|
||||
class DatabaseHelper
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param ConnectionInterface $db The database connection
|
||||
*/
|
||||
public function __construct(private ConnectionInterface $db) {}
|
||||
|
||||
/**
|
||||
* Generate cross-database string aggregation SQL
|
||||
*
|
||||
* Generates the appropriate SQL for concatenating strings from multiple rows:
|
||||
* - MySQL: GROUP_CONCAT(column SEPARATOR ',')
|
||||
* - PostgreSQL: STRING_AGG(CAST(column AS TEXT), ',')
|
||||
* - MS SQL: STRING_AGG(CAST(column AS NVARCHAR(MAX)), ',')
|
||||
*
|
||||
* @param string $column The column name to aggregate
|
||||
* @param string $separator The separator to use between values (default: ',')
|
||||
* @return string The database-specific SQL string
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function stringAggregate(string $column, string $separator = ','): string
|
||||
{
|
||||
return match ($this->db->getDriverName()) {
|
||||
'mysql' => "GROUP_CONCAT({$column} SEPARATOR '{$separator}')",
|
||||
'pgsql' => "STRING_AGG(CAST({$column} AS TEXT), '{$separator}')",
|
||||
'sqlsrv' => "STRING_AGG(CAST({$column} AS NVARCHAR(MAX)), '{$separator}')",
|
||||
default => "GROUP_CONCAT({$column} SEPARATOR '{$separator}')", // fallback to MySQL syntax
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cross-database week number extraction SQL
|
||||
*
|
||||
* Generates the appropriate SQL for extracting the week number from a date:
|
||||
* - MySQL: WEEK(column)
|
||||
* - PostgreSQL: EXTRACT(WEEK FROM column)::integer
|
||||
* - MS SQL: DATEPART(week, column)
|
||||
*
|
||||
* @param string $column The column name containing the date
|
||||
* @return string The database-specific SQL string
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function weekNumber(string $column): string
|
||||
{
|
||||
return match ($this->db->getDriverName()) {
|
||||
'mysql' => "WEEK({$column})",
|
||||
'pgsql' => "EXTRACT(WEEK FROM {$column})::integer",
|
||||
'sqlsrv' => "DATEPART(week, {$column})",
|
||||
default => "WEEK({$column})", // fallback to MySQL syntax
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse status group SQL strings to arrays
|
||||
*
|
||||
* Converts status group SQL strings like 'IN(0,-1,3)' to integer arrays [0, -1, 3]
|
||||
* This is used to convert legacy SQL-based status groups to Query Builder compatible arrays.
|
||||
*
|
||||
* @param array $statusGroupsSQL Associative array with status group names as keys and SQL strings as values
|
||||
* @return array Associative array with status group names as keys and integer arrays as values
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function parseStatusGroups(array $statusGroupsSQL): array
|
||||
{
|
||||
$statusGroups = [];
|
||||
|
||||
foreach ($statusGroupsSQL as $key => $sqlString) {
|
||||
// Match patterns like "IN(0,-1,3)" or "IN (0, -1, 3)"
|
||||
if (preg_match('/IN\s*\(([\d,\s-]+)\)/', $sqlString, $matches)) {
|
||||
// Split by comma, trim whitespace, convert to integers
|
||||
$values = explode(',', $matches[1]);
|
||||
$statusGroups[$key] = array_map(fn ($val) => (int) trim($val), $values);
|
||||
} else {
|
||||
// If pattern doesn't match, return empty array
|
||||
$statusGroups[$key] = [];
|
||||
}
|
||||
}
|
||||
|
||||
return $statusGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cross-database date formatting SQL
|
||||
*
|
||||
* Generates the appropriate SQL for formatting dates:
|
||||
* - MySQL: DATE_FORMAT(column, format)
|
||||
* - PostgreSQL: TO_CHAR(column, format)
|
||||
* - MS SQL: FORMAT(column, format)
|
||||
*
|
||||
* Note: The format string is converted from MySQL format to PostgreSQL/MS SQL format when needed
|
||||
*
|
||||
* @param string $column The column name containing the date
|
||||
* @param string $format The format string (MySQL DATE_FORMAT syntax)
|
||||
* @return string The database-specific SQL string
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function formatDate(string $column, string $format): string
|
||||
{
|
||||
return match ($this->db->getDriverName()) {
|
||||
'mysql' => "DATE_FORMAT({$column}, '{$format}')",
|
||||
'pgsql' => "TO_CHAR({$column}, '{$this->convertDateFormatToPostgres($format)}')",
|
||||
'sqlsrv' => "FORMAT({$column}, '{$this->convertDateFormatToMsSql($format)}')",
|
||||
default => "DATE_FORMAT({$column}, '{$format}')", // fallback to MySQL syntax
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert MySQL DATE_FORMAT format string to PostgreSQL TO_CHAR format
|
||||
*
|
||||
* @param string $mysqlFormat MySQL format string
|
||||
* @return string PostgreSQL format string
|
||||
*/
|
||||
private function convertDateFormatToPostgres(string $mysqlFormat): string
|
||||
{
|
||||
// Common MySQL to PostgreSQL format conversions
|
||||
$conversions = [
|
||||
'%Y' => 'YYYY', // 4-digit year
|
||||
'%y' => 'YY', // 2-digit year
|
||||
'%m' => 'MM', // Month number (01-12)
|
||||
'%d' => 'DD', // Day of month (01-31)
|
||||
'%e' => 'FMDD', // Day of month (1-31) without leading zero
|
||||
'%H' => 'HH24', // Hour (00-23)
|
||||
'%i' => 'MI', // Minutes (00-59)
|
||||
'%s' => 'SS', // Seconds (00-59)
|
||||
'%W' => 'Day', // Weekday name
|
||||
'%M' => 'Month', // Month name
|
||||
];
|
||||
|
||||
return str_replace(array_keys($conversions), array_values($conversions), $mysqlFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert MySQL DATE_FORMAT format string to MS SQL FORMAT format
|
||||
*
|
||||
* @param string $mysqlFormat MySQL format string
|
||||
* @return string MS SQL format string
|
||||
*/
|
||||
private function convertDateFormatToMsSql(string $mysqlFormat): string
|
||||
{
|
||||
// Common MySQL to MS SQL format conversions
|
||||
$conversions = [
|
||||
'%Y' => 'yyyy', // 4-digit year
|
||||
'%y' => 'yy', // 2-digit year
|
||||
'%m' => 'MM', // Month number (01-12)
|
||||
'%d' => 'dd', // Day of month (01-31)
|
||||
'%e' => 'd', // Day of month (1-31) without leading zero
|
||||
'%H' => 'HH', // Hour (00-23)
|
||||
'%i' => 'mm', // Minutes (00-59)
|
||||
'%s' => 'ss', // Seconds (00-59)
|
||||
'%W' => 'dddd', // Weekday name
|
||||
'%M' => 'MMMM', // Month name
|
||||
];
|
||||
|
||||
return str_replace(array_keys($conversions), array_values($conversions), $mysqlFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cross-database SQL for yesterday's date
|
||||
*
|
||||
* Generates the appropriate SQL for getting yesterday's date:
|
||||
* - MySQL: DATE(NOW() - INTERVAL 1 DAY)
|
||||
* - PostgreSQL: (CURRENT_DATE - INTERVAL '1 day')::date
|
||||
* - MS SQL: CAST(DATEADD(day, -1, GETDATE()) AS DATE)
|
||||
*
|
||||
* @return string The database-specific SQL string
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function yesterdayDate(): string
|
||||
{
|
||||
return match ($this->db->getDriverName()) {
|
||||
'mysql' => 'DATE(NOW() - INTERVAL 1 DAY)',
|
||||
'pgsql' => "(CURRENT_DATE - INTERVAL '1 day')::date",
|
||||
'sqlsrv' => 'CAST(DATEADD(day, -1, GETDATE()) AS DATE)',
|
||||
default => 'DATE(NOW() - INTERVAL 1 DAY)', // fallback to MySQL syntax
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cross-database SQL for the current date
|
||||
*
|
||||
* Generates the appropriate SQL for getting the current date:
|
||||
* - MySQL: CURDATE()
|
||||
* - PostgreSQL: CURRENT_DATE
|
||||
* - MS SQL: CAST(GETDATE() AS DATE)
|
||||
*
|
||||
* @return string The database-specific SQL string
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function currentDate(): string
|
||||
{
|
||||
return match ($this->db->getDriverName()) {
|
||||
'mysql' => 'CURDATE()',
|
||||
'pgsql' => 'CURRENT_DATE',
|
||||
'sqlsrv' => 'CAST(GETDATE() AS DATE)',
|
||||
default => 'CURDATE()', // fallback to MySQL syntax
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cross-database SQL for date comparison with yesterday
|
||||
*
|
||||
* Generates the appropriate SQL for comparing a date column with yesterday's date:
|
||||
* - MySQL: DATE(column) = DATE(NOW() - INTERVAL 1 DAY)
|
||||
* - PostgreSQL: column::date = (CURRENT_DATE - INTERVAL '1 day')::date
|
||||
* - MS SQL: CAST(column AS DATE) = CAST(DATEADD(day, -1, GETDATE()) AS DATE)
|
||||
*
|
||||
* @param string $column The column name containing the date
|
||||
* @return string The database-specific SQL string
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function isYesterday(string $column): string
|
||||
{
|
||||
return match ($this->db->getDriverName()) {
|
||||
'mysql' => "DATE({$column}) = DATE(NOW() - INTERVAL 1 DAY)",
|
||||
'pgsql' => "{$column}::date = (CURRENT_DATE - INTERVAL '1 day')::date",
|
||||
'sqlsrv' => "CAST({$column} AS DATE) = CAST(DATEADD(day, -1, GETDATE()) AS DATE)",
|
||||
default => "DATE({$column}) = DATE(NOW() - INTERVAL 1 DAY)", // fallback to MySQL syntax
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current database driver name
|
||||
*
|
||||
* @return string The driver name ('mysql', 'pgsql', 'sqlsrv', etc.)
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function getDriverName(): string
|
||||
{
|
||||
return $this->db->getDriverName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cross-database SQL for FIND_IN_SET functionality
|
||||
*
|
||||
* Searches for a value in a comma-separated string field:
|
||||
* - MySQL: FIND_IN_SET(needle, haystack)
|
||||
* - PostgreSQL: needle = ANY(STRING_TO_ARRAY(haystack, ','))
|
||||
* - MS SQL: CHARINDEX(',' + needle + ',', ',' + haystack + ',') > 0
|
||||
*
|
||||
* @param string $needle The value to search for (use '?' for parameter binding)
|
||||
* @param string $haystack The column containing comma-separated values
|
||||
* @return string The database-specific SQL string
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function findInSet(string $needle, string $haystack): string
|
||||
{
|
||||
return match ($this->db->getDriverName()) {
|
||||
'mysql' => "FIND_IN_SET({$needle}, {$haystack})",
|
||||
'pgsql' => "{$needle} = ANY(STRING_TO_ARRAY({$haystack}, ','))",
|
||||
'sqlsrv' => "CHARINDEX(',' + CAST({$needle} AS NVARCHAR) + ',', ',' + {$haystack} + ',') > 0",
|
||||
default => "FIND_IN_SET({$needle}, {$haystack})", // fallback to MySQL syntax
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cross-database SQL for current timestamp
|
||||
*
|
||||
* Generates the appropriate SQL for the current date and time:
|
||||
* - MySQL: NOW()
|
||||
* - PostgreSQL: CURRENT_TIMESTAMP
|
||||
* - MS SQL: GETDATE()
|
||||
*
|
||||
* @return string The database-specific SQL string
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function currentTimestamp(): string
|
||||
{
|
||||
return match ($this->db->getDriverName()) {
|
||||
'mysql' => 'NOW()',
|
||||
'pgsql' => 'CURRENT_TIMESTAMP',
|
||||
'sqlsrv' => 'GETDATE()',
|
||||
default => 'NOW()', // fallback to MySQL syntax
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cross-database SQL for IFNULL/COALESCE functionality
|
||||
*
|
||||
* Returns the first non-null value:
|
||||
* - MySQL: IFNULL(expr, default)
|
||||
* - PostgreSQL: COALESCE(expr, default)
|
||||
* - MS SQL: COALESCE(expr, default)
|
||||
*
|
||||
* Note: COALESCE is ANSI SQL standard and works on all databases,
|
||||
* but this method is provided for explicit IFNULL replacement.
|
||||
*
|
||||
* @param string $expr The expression to check for null
|
||||
* @param string $default The default value if expr is null
|
||||
* @return string The database-specific SQL string
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function ifNull(string $expr, string $default): string
|
||||
{
|
||||
return match ($this->db->getDriverName()) {
|
||||
'mysql' => "IFNULL({$expr}, {$default})",
|
||||
'pgsql', 'sqlsrv' => "COALESCE({$expr}, {$default})",
|
||||
default => "IFNULL({$expr}, {$default})", // fallback to MySQL syntax
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cross-database SQL for IF/CASE functionality
|
||||
*
|
||||
* Replaces MySQL's IF(condition, then, else) with standard CASE WHEN:
|
||||
* - MySQL: IF(condition, then, else)
|
||||
* - PostgreSQL/MS SQL: CASE WHEN condition THEN then ELSE else END
|
||||
*
|
||||
* Note: This method always returns CASE WHEN syntax which is ANSI SQL standard
|
||||
* and works on all databases. Use this for cross-database compatibility.
|
||||
*
|
||||
* @param string $condition The condition to evaluate
|
||||
* @param string $then The value if condition is true
|
||||
* @param string $else The value if condition is false
|
||||
* @return string The CASE WHEN SQL string (works on all databases)
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function ifThen(string $condition, string $then, string $else): string
|
||||
{
|
||||
// CASE WHEN is ANSI SQL standard and works on all databases
|
||||
return "CASE WHEN {$condition} THEN {$then} ELSE {$else} END";
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a column or alias identifier with the correct quoting for the current database
|
||||
*
|
||||
* Uses the connection grammar to produce the correct identifier quoting:
|
||||
* - MySQL: backticks (`identifier`)
|
||||
* - PostgreSQL: double quotes ("identifier")
|
||||
* - MS SQL: square brackets ([identifier])
|
||||
*
|
||||
* Supports dotted notation for table-qualified columns (e.g. 'table.column').
|
||||
*
|
||||
* @param string $identifier The column, alias, or table.column identifier to wrap
|
||||
* @return string The properly quoted identifier
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function wrapColumn(string $identifier): string
|
||||
{
|
||||
return $this->db->getQueryGrammar()->wrap($identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cross-database CAST expression
|
||||
*
|
||||
* Maps abstract type names to database-specific CAST target types:
|
||||
* - 'text': MySQL -> CHAR, PostgreSQL -> TEXT, MS SQL -> NVARCHAR(MAX)
|
||||
* - 'integer': MySQL -> SIGNED, PostgreSQL -> INTEGER, MS SQL -> INT
|
||||
* - 'decimal': MySQL -> DECIMAL(precision,scale), PostgreSQL/MS SQL -> NUMERIC(precision,scale)
|
||||
*
|
||||
* @param string $expression The SQL expression to cast
|
||||
* @param string $type The abstract type: 'text', 'integer', or 'decimal'
|
||||
* @param int $precision Precision for decimal type (default: 10)
|
||||
* @param int $scale Scale for decimal type (default: 2)
|
||||
* @return string The database-specific CAST expression
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function castAs(string $expression, string $type, int $precision = 10, int $scale = 2): string
|
||||
{
|
||||
$driver = $this->db->getDriverName();
|
||||
|
||||
$targetType = match ($type) {
|
||||
'text' => match ($driver) {
|
||||
'mysql' => 'CHAR',
|
||||
'pgsql' => 'TEXT',
|
||||
'sqlsrv' => 'NVARCHAR(MAX)',
|
||||
default => 'CHAR',
|
||||
},
|
||||
'integer' => match ($driver) {
|
||||
'mysql' => 'SIGNED',
|
||||
'pgsql' => 'INTEGER',
|
||||
'sqlsrv' => 'INT',
|
||||
default => 'SIGNED',
|
||||
},
|
||||
'decimal' => match ($driver) {
|
||||
'mysql' => "DECIMAL({$precision},{$scale})",
|
||||
'pgsql' => "NUMERIC({$precision},{$scale})",
|
||||
'sqlsrv' => "DECIMAL({$precision},{$scale})",
|
||||
default => "DECIMAL({$precision},{$scale})",
|
||||
},
|
||||
default => throw new \InvalidArgumentException("Unsupported cast type: {$type}. Use 'text', 'integer', or 'decimal'."),
|
||||
};
|
||||
|
||||
return "CAST({$expression} AS {$targetType})";
|
||||
}
|
||||
}
|
||||
107
app/Core/Db/Db.php
Normal file
107
app/Core/Db/Db.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\Db;
|
||||
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Illuminate\Database\DatabaseManager;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Leantime\Core\Events\DispatchesEvents;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Database Class - Very simple abstraction layer for pdo connection
|
||||
*/
|
||||
class Db
|
||||
{
|
||||
use DispatchesEvents;
|
||||
|
||||
/**
|
||||
* @var ConnectionInterface Laravel database connection
|
||||
*/
|
||||
private ConnectionInterface $connection;
|
||||
|
||||
/**
|
||||
* @var DatabaseManager Laravel's database manager
|
||||
*/
|
||||
private DatabaseManager $dbManager;
|
||||
|
||||
/**
|
||||
* __construct - connect to database and select database
|
||||
*
|
||||
* @param object $app Application container
|
||||
* @param string|null $connection Connection name (defaults to configured default connection)
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($app, ?string $connection = null)
|
||||
{
|
||||
// Get Laravel's database manager from the container
|
||||
$this->dbManager = $app['db'];
|
||||
|
||||
// Use the configured default connection if none specified
|
||||
$connection = $connection ?? $app['config']->get('database.default', 'mysql');
|
||||
|
||||
// Get a connection from the manager
|
||||
try {
|
||||
$this->connection = $this->dbManager->connection($connection);
|
||||
} catch (\PDOException $e) {
|
||||
Log::error("Can't connect to database");
|
||||
throw new \Exception($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PDO connection (lazily retrieved from Laravel's connection pool)
|
||||
*
|
||||
* @return \PDO|null
|
||||
*/
|
||||
public function __get($name)
|
||||
{
|
||||
if ($name === 'database') {
|
||||
return $this->connection->getPdo();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Laravel ConnectionInterface
|
||||
*/
|
||||
public function getConnection(): ConnectionInterface
|
||||
{
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function will generate a PDO binding string (":editors0,:editors1,:editors2,:editors3") to be used in a PDO
|
||||
* query that uses the IN() clause, to assist in proper PDO array bindings to avoid SQL injection.
|
||||
*
|
||||
* A counted for loop is used rather than foreach with a key to avoid issues if the array passed has any
|
||||
* arbitrary keys
|
||||
*/
|
||||
public static function arrayToPdoBindingString(string $name, int $count): string
|
||||
{
|
||||
$bindingStatement = '';
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$bindingStatement .= ':'.$name.$i;
|
||||
if ($i != $count - 1) {
|
||||
$bindingStatement .= ',';
|
||||
}
|
||||
}
|
||||
|
||||
return $bindingStatement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a string to only contain letters, numbers and underscore.
|
||||
* Used for patch statements with variable column keys values
|
||||
*/
|
||||
public static function sanitizeToColumnString(string $string): string
|
||||
{
|
||||
return preg_replace('/[^a-zA-Z0-9_]/', '', $string);
|
||||
}
|
||||
|
||||
public static function sanitizeComparitorString(string $string): string
|
||||
{
|
||||
return preg_replace('/[^=<>LIKENOT]/', '', $string);
|
||||
}
|
||||
}
|
||||
15
app/Core/Db/DbColumn.php
Normal file
15
app/Core/Db/DbColumn.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\Db;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute]
|
||||
class DbColumn
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
) {
|
||||
//
|
||||
}
|
||||
}
|
||||
369
app/Core/Db/Repository.php
Normal file
369
app/Core/Db/Repository.php
Normal file
@@ -0,0 +1,369 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\Db;
|
||||
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Leantime\Core\Database;
|
||||
use Leantime\Core\Events\DispatchesEvents;
|
||||
use PDO;
|
||||
use PDOStatement;
|
||||
use ReflectionClass;
|
||||
use ReflectionProperty;
|
||||
|
||||
/**
|
||||
* Repository
|
||||
*/
|
||||
abstract class Repository
|
||||
{
|
||||
use DispatchesEvents;
|
||||
|
||||
protected string $entity;
|
||||
|
||||
protected string $model;
|
||||
|
||||
/**
|
||||
* dbcall - creates a new dbcall object
|
||||
*
|
||||
* @param array $args - usually the value of func_get_args(), gives events/filters values to work with
|
||||
*/
|
||||
protected function dbcall(array ...$args): object
|
||||
{
|
||||
return new class($args, $this)
|
||||
{
|
||||
private ?PDOStatement $stmn = null;
|
||||
|
||||
private array $args;
|
||||
|
||||
private Repository $caller_class;
|
||||
|
||||
/**
|
||||
* @var \Closure|mixed|object|null
|
||||
*/
|
||||
private mixed $db;
|
||||
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param array $args - usually the value of func_get_args(), gives events/filters values to work with
|
||||
* @param Repository $caller_class - the class object that was called
|
||||
*/
|
||||
public function __construct(array $args, Repository $caller_class)
|
||||
{
|
||||
$this->args = $args;
|
||||
$this->caller_class = $caller_class;
|
||||
// Use the singleton instance of Db to ensure connection pooling
|
||||
$this->db = app()->get(Db::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* prepares sql for entry; wrapper for PDO\prepare()
|
||||
*
|
||||
* @param array $args - additional arguments to pass along to prepare function
|
||||
*/
|
||||
public function prepare(string $sql, array $args = []): void
|
||||
{
|
||||
$sql = $this->caller_class::dispatch_filter(
|
||||
'sql',
|
||||
$sql,
|
||||
$this->getArgs(['prepareArgs' => $args]),
|
||||
4
|
||||
);
|
||||
|
||||
$this->stmn = $this->db->database->prepare($sql, $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* binds values for search/replace of sql; wrapper for PDO\bindValue()
|
||||
*
|
||||
* @param string $needle - placeholder to replace
|
||||
* @param string $replace - value to replace with
|
||||
* @param int $type - type of value being replaced
|
||||
*/
|
||||
public function bindValue(string $needle, mixed $replace, int $type = PDO::PARAM_STR): void
|
||||
{
|
||||
$replace = $this->caller_class::dispatch_filter(
|
||||
'binding.'.str_replace(':', '', $needle),
|
||||
$replace,
|
||||
$this->getArgs(),
|
||||
4
|
||||
);
|
||||
|
||||
$this->stmn->bindValue($needle, $replace, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* executes the sql call - uses \PDO
|
||||
*/
|
||||
public function lastInsertId(): mixed
|
||||
{
|
||||
return $this->db->database->lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* executes the sql call - uses \PDO
|
||||
*/
|
||||
public function setFetchMode($mode, $class): bool
|
||||
{
|
||||
return $this->stmn->setFetchMode($mode, $class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the arguments to pass along to events/filter
|
||||
*
|
||||
* @param array $additions - any other additional parameters to include
|
||||
*/
|
||||
private function getArgs(array $additions = []): array
|
||||
{
|
||||
$args = array_merge($this->args, ['self' => $this]);
|
||||
|
||||
if (! empty($additions)) {
|
||||
$args = array_merge($args, $additions);
|
||||
}
|
||||
|
||||
$this->caller_class::dispatch_filter('args', $args, [], 5);
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* executes the sql call - uses \PDO
|
||||
*/
|
||||
public function __call(string $method, $arguments): mixed
|
||||
{
|
||||
if (! isset($this->stmn)) {
|
||||
throw new \Error("You must run the 'prepare' method first!");
|
||||
}
|
||||
|
||||
if (! in_array($method, ['execute', 'fetch', 'fetchAll'])) {
|
||||
throw new \Error('Method does not exist');
|
||||
}
|
||||
|
||||
$this->caller_class::dispatch_event('beforeExecute', $this->getArgs(), 4);
|
||||
|
||||
$this->stmn = $this->caller_class::dispatch_filter('stmn', $this->stmn, $this->getArgs(), 4);
|
||||
$method = $this->caller_class::dispatch_filter('method', $method, $this->getArgs(), 4);
|
||||
|
||||
try {
|
||||
$values = $this->stmn->execute();
|
||||
|
||||
if (in_array($method, ['fetch', 'fetchAll'])) {
|
||||
$values = $this->stmn->$method();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Ensure cursor is closed even on exceptions
|
||||
if (isset($this->stmn)) {
|
||||
$this->stmn->closeCursor();
|
||||
}
|
||||
throw $e;
|
||||
} finally {
|
||||
// Always ensure proper cleanup
|
||||
if (isset($this->stmn)) {
|
||||
$this->stmn->closeCursor();
|
||||
}
|
||||
}
|
||||
|
||||
$this->caller_class::dispatch_event('afterExecute', $this->getArgs(), 4);
|
||||
|
||||
return $this->caller_class::dispatch_filter('return', $values, $this->getArgs(), 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor to ensure proper cleanup of database resources
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if (isset($this->stmn)) {
|
||||
try {
|
||||
$this->stmn->closeCursor();
|
||||
} catch (\Exception $e) {
|
||||
// Silently handle any cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* patch - updates a record in the database
|
||||
*
|
||||
* @param int $id - the id of the record to update
|
||||
* @param array $params - the parameters to update
|
||||
*/
|
||||
public function patch(int $id, array $params): bool
|
||||
{
|
||||
|
||||
unset($params['act']);
|
||||
|
||||
if ($this->entity == '') {
|
||||
report('Patch not implemented for this entity');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$sql = 'UPDATE zp_'.$this->entity.' SET ';
|
||||
|
||||
foreach ($params as $key => $value) {
|
||||
$sql .= ''.Db::sanitizeToColumnString($key).'=:'.Db::sanitizeToColumnString($key).', ';
|
||||
}
|
||||
|
||||
$sql .= 'id=:id WHERE id=:id LIMIT 1';
|
||||
|
||||
$call = $this->dbcall(func_get_args());
|
||||
|
||||
$call->prepare($sql);
|
||||
|
||||
$call->bindValue(':id', $id, PDO::PARAM_STR);
|
||||
|
||||
foreach ($params as $key => $value) {
|
||||
$call->bindValue(':'.Db::sanitizeToColumnString($key), $value, PDO::PARAM_STR);
|
||||
}
|
||||
|
||||
return $call->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
public function insert(object $objectToInsert): false|int
|
||||
{
|
||||
|
||||
if ($this->entity == '') {
|
||||
report('Insert not implemented for this entity');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$sql = 'INSERT INTO zp_'.$this->entity.' (';
|
||||
|
||||
$sqlArr = [];
|
||||
foreach ($objectToInsert as $key => $value) {
|
||||
if ($this->getFieldAttribute($objectToInsert, $key)) {
|
||||
$sqlArr[] = '`'.Db::sanitizeToColumnString($key).'`';
|
||||
}
|
||||
}
|
||||
$sql .= implode(',', $sqlArr);
|
||||
|
||||
$sql .= ') VALUES (';
|
||||
|
||||
$sqlArr2 = [];
|
||||
foreach ($objectToInsert as $key => $value) {
|
||||
if ($this->getFieldAttribute($objectToInsert, $key)) {
|
||||
$sqlArr2[] = ':'.Db::sanitizeToColumnString($key).'';
|
||||
}
|
||||
}
|
||||
$sql .= implode(',', $sqlArr2);
|
||||
|
||||
$sql .= ')';
|
||||
|
||||
$call = $this->dbcall(func_get_args());
|
||||
|
||||
$call->prepare($sql);
|
||||
|
||||
foreach ($objectToInsert as $key => $value) {
|
||||
if ($this->getFieldAttribute($objectToInsert, $key)) {
|
||||
$call->bindValue(':'.Db::sanitizeToColumnString($key), $value, PDO::PARAM_STR);
|
||||
}
|
||||
}
|
||||
|
||||
$call->execute();
|
||||
|
||||
return $call->lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* delete - deletes a record from the database
|
||||
*
|
||||
* @param int $id - the id of the record to delete
|
||||
*/
|
||||
public function delete(int $id): void {}
|
||||
|
||||
/**
|
||||
* get - gets a record from the database
|
||||
*
|
||||
* @param int $id - the id of the record to get
|
||||
*
|
||||
* @throws BindingResolutionException
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
public function get(int $id): mixed
|
||||
{
|
||||
if ($this->entity == '' || $this->model == '') {
|
||||
report('Get not implemented for this entity');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$sql = 'SELECT ';
|
||||
|
||||
$entityModel = app()->make($this->model);
|
||||
$dbFields = $this->getDbFields($this->model);
|
||||
|
||||
$sql .= implode(',', $dbFields);
|
||||
|
||||
$sql .= ' FROM zp_'.$this->entity.' WHERE id = :id ';
|
||||
|
||||
$call = $this->dbcall(func_get_args());
|
||||
|
||||
$call->prepare($sql);
|
||||
|
||||
$call->bindValue(':id', $id, PDO::PARAM_STR);
|
||||
|
||||
$call->execute();
|
||||
|
||||
$call->setFetchMode(PDO::FETCH_CLASS, $this->model);
|
||||
|
||||
return $call->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* getFieldAttribute - gets the field attribute for a given property
|
||||
*
|
||||
* @param object|string $class - the class to get the attribute from
|
||||
* @param string $property - the property to get the attribute from
|
||||
* @param bool $includeId - whether or not to include the id attribute
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
protected function getFieldAttribute(object|string $class, string $property, bool $includeId = false): array|false
|
||||
{
|
||||
// Don't create or update id attributes
|
||||
if ($includeId === false && $property == 'id') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$property = new ReflectionProperty($class, $property);
|
||||
|
||||
$attributes = $property->getAttributes();
|
||||
foreach ($attributes as $attribute) {
|
||||
$name = $attribute->getName();
|
||||
if (str_contains($name, 'DbColumn')) {
|
||||
return $attribute->getArguments();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* getDbFields - gets the database fields for a given class
|
||||
*
|
||||
* @param object|string $class - the class to get the fields from
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
protected function getDbFields(object|string $class): array
|
||||
{
|
||||
$property = new ReflectionClass($class);
|
||||
|
||||
$properties = $property->getProperties();
|
||||
|
||||
$propertyArray = [];
|
||||
foreach ($properties as $property) {
|
||||
if ($this->getFieldAttribute($class, $property->getName(), true)) {
|
||||
$propertyArray[] = $property->getName();
|
||||
}
|
||||
}
|
||||
|
||||
return $propertyArray;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user