From 405c0fb6fd782bf0bdf31487990fd939e6c4f634 Mon Sep 17 00:00:00 2001 From: Jagepard Date: Tue, 14 Jul 2026 12:17:56 +0300 Subject: [PATCH] refactor(model): extract CRUD, Schema and Cache logic into traits --- README.md | 8 +- docs.md | 122 +++++++--- src/{Driver => Drivers}/MySQL.php | 21 +- src/{Driver => Drivers}/PgSQL.php | 26 +- src/{Driver => Drivers}/SQLite.php | 26 +- src/Entity.php | 5 +- src/Interfaces/SqlDialectInterface.php | 24 ++ src/Model.php | 5 +- src/QB.php | 22 +- src/Repository.php | 313 +------------------------ src/Traits/CacheTrait.php | 82 +++++++ src/Traits/CrudTrait.php | 194 +++++++++++++++ src/Traits/SchemaTrait.php | 73 ++++++ tests/RepositoryTest.php | 1 - 14 files changed, 545 insertions(+), 377 deletions(-) rename src/{Driver => Drivers}/MySQL.php (75%) rename src/{Driver => Drivers}/PgSQL.php (67%) rename src/{Driver => Drivers}/SQLite.php (66%) create mode 100644 src/Interfaces/SqlDialectInterface.php create mode 100644 src/Traits/CacheTrait.php create mode 100644 src/Traits/CrudTrait.php create mode 100644 src/Traits/SchemaTrait.php diff --git a/README.md b/README.md index bbc8f62..5cf2688 100755 --- a/README.md +++ b/README.md @@ -81,13 +81,13 @@ Define your database schema using the Query Builder. use Rudra\Model\Schema; Schema::create('users', function ($table) { - $table->integer('id', '', true) // auto-increment + $table->integer('id', autoincrement: true) ->string('name') ->string('email') ->text('bio', 'NULL') - ->created_at() - ->updated_at() - ->pk('id'); + ->createdAt() + ->updatedAt() + ->primaryKey('id'); })->execute(); ``` ### 5. Simple File Caching diff --git a/docs.md b/docs.md index 652bb9f..33b292f 100644 --- a/docs.md +++ b/docs.md @@ -1,62 +1,66 @@ ## Table of contents -- [Rudra\Model\Driver\MySQL](#rudra_model_driver_mysql) -- [Rudra\Model\Driver\PgSQL](#rudra_model_driver_pgsql) -- [Rudra\Model\Driver\SQLite](#rudra_model_driver_sqlite) +- [Rudra\Model\Drivers\MySQL](#rudra_model_drivers_mysql) +- [Rudra\Model\Drivers\PgSQL](#rudra_model_drivers_pgsql) +- [Rudra\Model\Drivers\SQLite](#rudra_model_drivers_sqlite) - [Rudra\Model\Entity](#rudra_model_entity) +- [Rudra\Model\Interfaces\SqlDialectInterface](#rudra_model_interfaces_sqldialectinterface) - [Rudra\Model\Model](#rudra_model_model) - [Rudra\Model\QB](#rudra_model_qb) - [Rudra\Model\QBFacade](#rudra_model_qbfacade) - [Rudra\Model\Repository](#rudra_model_repository) - [Rudra\Model\Schema](#rudra_model_schema) +- [Rudra\Model\Traits\CacheTrait](#rudra_model_traits_cachetrait) +- [Rudra\Model\Traits\CrudTrait](#rudra_model_traits_crudtrait) +- [Rudra\Model\Traits\SchemaTrait](#rudra_model_traits_schematrait) --- - + -### Class: Rudra\Model\Driver\MySQL +### Class: Rudra\Model\Drivers\MySQL | Visibility | Function | |:-----------|:---------| -| public | `concat(string $fieldName, string $alias, ?string $orderBy): string`
| +| public | `groupConcat(string $fieldName, string $alias, ?string $orderBy): string`
| | public | `close(): string`
| | public | `integer(string $field, string $default, bool $autoincrement, string $null): string`
| | public | `string(string $field, string $default, string $null): string`
| | public | `text(string $field, string $null): string`
| -| public | `created_at(): string`
| -| public | `updated_at(): string`
| -| public | `pk(string $field): string`
| +| public | `createdAt(): string`
| +| public | `updatedAt(): string`
| +| public | `primaryKey(string $field): string`
| - + -### Class: Rudra\Model\Driver\PgSQL +### Class: Rudra\Model\Drivers\PgSQL | Visibility | Function | |:-----------|:---------| -| public | `concat(string $fieldName, string $alias, ?string $orderBy): string`
| +| public | `groupConcat(string $fieldName, string $alias, ?string $orderBy): string`
| | public | `close(): string`
| -| public | `integer(string $field, string $default, bool $pk, string $null): string`
| +| public | `integer(string $field, string $default, bool $autoincrement, string $null): string`
| | public | `string(string $field, string $default, string $null): string`
| | public | `text(string $field, string $null): string`
| -| public | `created_at(): string`
| -| public | `updated_at(): string`
| -| public | `pk(string $field): string`
| +| public | `createdAt(): string`
| +| public | `updatedAt(): string`
| +| public | `primaryKey(string $field): string`
| - + -### Class: Rudra\Model\Driver\SQLite +### Class: Rudra\Model\Drivers\SQLite | Visibility | Function | |:-----------|:---------| -| public | `concat(string $fieldName, string $alias, ?string $orderBy): string`
| +| public | `groupConcat(string $fieldName, string $alias, ?string $orderBy): string`
| | public | `close(): string`
| -| public | `integer(string $field, string $default, bool $pk, string $null): string`
| +| public | `integer(string $field, string $default, bool $autoincrement, string $null): string`
| | public | `string(string $field, string $default, string $null): string`
| | public | `text(string $field, string $null): string`
| -| public | `created_at(): string`
| -| public | `updated_at(): string`
| -| public | `pk(string $field): string`
| +| public | `createdAt(): string`
| +| public | `updatedAt(): string`
| +| public | `primaryKey(string $field): string`
| @@ -69,6 +73,21 @@ | protected static | `callMethod(string $method, array $parameters): mixed`
Dynamically calls a method on the corresponding Model, Repository, or parent Repository class.
The method first attempts to call the method on the Model class associated with the Entity.
If the Model does not exist, it falls back to the Repository class.
If the Repository does not exist, it defaults to the parent Repository class. | + + +### Class: Rudra\Model\Interfaces\SqlDialectInterface +| Visibility | Function | +|:-----------|:---------| +| abstract public | `groupConcat(string $fieldName, string $alias, ?string $orderBy): string`
| +| abstract public | `close(): string`
| +| abstract public | `integer(string $field, string $default, bool $autoincrement, string $null): string`
| +| abstract public | `string(string $field, string $default, string $null): string`
| +| abstract public | `text(string $field, string $null): string`
| +| abstract public | `createdAt(): string`
| +| abstract public | `updatedAt(): string`
| +| abstract public | `primaryKey(string $field): string`
| + + ### Class: Rudra\Model\Model @@ -83,9 +102,9 @@ ### Class: Rudra\Model\QB | Visibility | Function | |:-----------|:---------| -| public | `__construct(?PDO $connection)`
Initializes the database driver based on the provided connection or a default connection from the container.
If no connection is provided and none is available in the container, a LogicException is thrown.
The driver is selected based on the database type specified in the connection's driver attribute. | +| public | `__construct($connection)`
Initializes the database driver based on the provided connection or a default connection from the container.
If no connection is provided and none is available in the container, a LogicException is thrown.
The driver is selected based on the database type specified in the connection's driver attribute. | | public | `select(string $fields): self`
| -| public | `concat(string $fieldName, string $alias, ?string $orderBy): self`
| +| public | `groupConcat(string $fieldName, string $alias, ?string $orderBy): self`
| | public | `from(string $table): self`
| | public | `where(string $param): self`
| | public | `and(string $param): self`
| @@ -102,9 +121,9 @@ | public | `integer(string $field, string $default, bool $autoincrement, string $null): self`
| | public | `string(string $field, string $default, string $null): self`
| | public | `text(string $field, string $null): self`
| -| public | `created_at(): self`
| -| public | `updated_at(): self`
| -| public | `pk(?string $field): self`
| +| public | `createdAt(): self`
| +| public | `updatedAt(): self`
| +| public | `primaryKey(?string $field): self`
| @@ -128,21 +147,21 @@ | public | `withConnection(PDO $connection): self`
Creates and returns a new instance of the class with the specified connection.
This method allows changing the connection while preserving the current table name.
It is useful for creating new instances with different database connections without modifying the original object. | | public | `qBuilder(string $queryString, array $queryParams): array`
Executes a custom SQL query and returns the result as an associative array.
The method prepares the query, executes it with optional parameters, and fetches all results. | | public | `getAllPerPage(Rudra\Pagination $pagination, ?string $fields): array`
| -| public | `find(string\|int $id): array\|false`
| | public | `getAll(string $sort, ?string $fields): array`
| | public | `numRows(): int`
| -| public | `findBy(string $field, mixed $value): array\|false`
Finds a single record by a specified field and value.
The field name is validated against the actual table columns to prevent SQL injection. | +| public | `findBy(string $field, mixed $value): array\|false`
Finds a single record by a specified field and value.
WARNING: The \$field parameter is inserted directly into the SQL query.
It is the developer's responsibility to ensure the field name is valid
and sanitized to prevent SQL injection. Do not pass raw user input here. | | public | `lastInsertId(): string`
| +| public | `search(string $search, string $column, ?string $fields): array`
Searches for records in the database based on a search term and column.
WARNING: The \$column parameter is inserted directly into the SQL query.
Ensure it is sanitized to prevent SQL injection.
Results are ordered by ID in descending order and limited to 10 records. | +| public | `find(string\|int $id): array\|false`
| | public | `update(array $fields): void`
| | public | `create(array $fields): void`
| | public | `delete(string\|int $id): void`
| | protected static | `updateStmtString(array $fields): string`
Generates a string of fields and placeholders for an SQL UPDATE statement.
The method takes an array of fields and constructs a comma-separated list of "key=:key" pairs.
This string can be directly used in the SET clause of an SQL UPDATE query. | | protected static | `createStmtString(array $fields): array`
Generates two strings for an SQL INSERT statement: one for column names and one for placeholders.
The method takes an array of fields and constructs two comma-separated lists:
- A list of column names.
- A list of placeholders (prefixed with colons) for parameter binding.
These strings can be directly used in the SQL INSERT query. | -| public | `getColumns(): array`
Retrieves the column information for the current table based on the database driver.
The method executes a query specific to the database type (MySQL, PostgreSQL, or SQLite)
to fetch the column details of the table. | -| public | `getFields(?string $fields): array`
Retrieves the list of fields (columns) for the current table.
If no specific fields are provided, the method fetches all column names based on the database driver.
Otherwise, it splits the provided comma-separated string of fields into an array. | -| public | `search(string $search, string $column, ?string $fields): array`
Searches for records in the database based on a search term and column.
The column name is validated against the actual table columns to prevent SQL injection.
Results are ordered by ID in descending order and limited to 10 records. | | public | `cache(array $params, ?string $cacheTime): mixed`
Caches the result of a method call to a JSON file for a specified duration.
If the cached file exists and is still valid (based on cache time), the cached data is returned.
Otherwise, the method executes the specified method, caches its result, and returns the data. | | public | `clearCache(string $type, ?string $key): void`
Clears cached files of a specified type or all types.
If a cache key is provided, only that specific cache file is deleted. | +| public | `getColumns(): array`
Retrieves the column information for the current table based on the database driver.
The method executes a query specific to the database type (MySQL, PostgreSQL, or SQLite)
to fetch the column details of the table. | +| public | `getFields(?string $fields): array`
Retrieves the list of fields (columns) for the current table.
If no specific fields are provided, the method fetches all column names based on the database driver.
Otherwise, it splits the provided comma-separated string of fields into an array. | @@ -154,6 +173,43 @@ | public | `execute(): bool`
Executes the schema creation by preparing and running the SQL query.
The SQL query is generated using the Query Builder and executed on the database connection. | + + +### Class: Rudra\Model\Traits\CacheTrait +| Visibility | Function | +|:-----------|:---------| +| public | `cache(array $params, ?string $cacheTime): mixed`
Caches the result of a method call to a JSON file for a specified duration.
If the cached file exists and is still valid (based on cache time), the cached data is returned.
Otherwise, the method executes the specified method, caches its result, and returns the data. | +| public | `clearCache(string $type, ?string $key): void`
Clears cached files of a specified type or all types.
If a cache key is provided, only that specific cache file is deleted. | + + + + +### Class: Rudra\Model\Traits\CrudTrait +| Visibility | Function | +|:-----------|:---------| +| public | `getAllPerPage(Rudra\Pagination $pagination, ?string $fields): array`
| +| public | `getAll(string $sort, ?string $fields): array`
| +| public | `numRows(): int`
| +| public | `findBy(string $field, mixed $value): array\|false`
Finds a single record by a specified field and value.
WARNING: The \$field parameter is inserted directly into the SQL query.
It is the developer's responsibility to ensure the field name is valid
and sanitized to prevent SQL injection. Do not pass raw user input here. | +| public | `lastInsertId(): string`
| +| public | `search(string $search, string $column, ?string $fields): array`
Searches for records in the database based on a search term and column.
WARNING: The \$column parameter is inserted directly into the SQL query.
Ensure it is sanitized to prevent SQL injection.
Results are ordered by ID in descending order and limited to 10 records. | +| public | `find(string\|int $id): array\|false`
| +| public | `update(array $fields): void`
| +| public | `create(array $fields): void`
| +| public | `delete(string\|int $id): void`
| +| protected static | `updateStmtString(array $fields): string`
Generates a string of fields and placeholders for an SQL UPDATE statement.
The method takes an array of fields and constructs a comma-separated list of "key=:key" pairs.
This string can be directly used in the SET clause of an SQL UPDATE query. | +| protected static | `createStmtString(array $fields): array`
Generates two strings for an SQL INSERT statement: one for column names and one for placeholders.
The method takes an array of fields and constructs two comma-separated lists:
- A list of column names.
- A list of placeholders (prefixed with colons) for parameter binding.
These strings can be directly used in the SQL INSERT query. | + + + + +### Class: Rudra\Model\Traits\SchemaTrait +| Visibility | Function | +|:-----------|:---------| +| public | `getColumns(): array`
Retrieves the column information for the current table based on the database driver.
The method executes a query specific to the database type (MySQL, PostgreSQL, or SQLite)
to fetch the column details of the table. | +| public | `getFields(?string $fields): array`
Retrieves the list of fields (columns) for the current table.
If no specific fields are provided, the method fetches all column names based on the database driver.
Otherwise, it splits the provided comma-separated string of fields into an array. | + + --- ###### created with [Rudra-Documentation-Collector](https://github.com/Jagepard/Rudra-Documentation-Collector) diff --git a/src/Driver/MySQL.php b/src/Drivers/MySQL.php similarity index 75% rename from src/Driver/MySQL.php rename to src/Drivers/MySQL.php index c1efca7..ab5d81e 100755 --- a/src/Driver/MySQL.php +++ b/src/Drivers/MySQL.php @@ -9,20 +9,25 @@ * @license https://mozilla.org/MPL/2.0/ MPL-2.0 */ -namespace Rudra\Model\Driver; +namespace Rudra\Model\Drivers; -class MySQL +use Rudra\Model\Interfaces\SqlDialectInterface; + +class MySQL implements SqlDialectInterface { - public function concat(string $fieldName, string $alias, ?string $orderBy): string + #[\Override] + public function groupConcat(string $fieldName, string $alias, ?string $orderBy): string { return ", GROUP_CONCAT($fieldName ORDER BY $orderBy SEPARATOR ';') as $alias "; } + #[\Override] public function close(): string { return ") ENGINE = InnoDB"; } + #[\Override] public function integer(string $field, string $default = "", bool $autoincrement = false, string $null = "NOT NULL"): string { if ($autoincrement) { @@ -32,27 +37,31 @@ public function integer(string $field, string $default = "", bool $autoincrement return ", `$field` INT $null $default"; } + #[\Override] public function string(string $field, string $default = "", string $null = "NOT NULL"): string { return ", `$field` VARCHAR(255) $null $default"; } + #[\Override] public function text(string $field, string $null = "NOT NULL"): string { return ", `$field` text $null"; } - public function created_at(): string + #[\Override] + public function createdAt(): string { return ", `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP"; } - public function updated_at(): string + #[\Override] + public function updatedAt(): string { return ", `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"; } - public function pk(string $field): string + public function primaryKey(string $field): string { return ", PRIMARY KEY (`$field`)"; } diff --git a/src/Driver/PgSQL.php b/src/Drivers/PgSQL.php similarity index 67% rename from src/Driver/PgSQL.php rename to src/Drivers/PgSQL.php index 7b340c5..2b2cd47 100755 --- a/src/Driver/PgSQL.php +++ b/src/Drivers/PgSQL.php @@ -9,50 +9,60 @@ * @license https://mozilla.org/MPL/2.0/ MPL-2.0 */ -namespace Rudra\Model\Driver; +namespace Rudra\Model\Drivers; -class PgSQL +use Rudra\Model\Interfaces\SqlDialectInterface; + +class PgSQL implements SqlDialectInterface { - public function concat(string $fieldName, string $alias, ?string $orderBy): string + #[\Override] + public function groupConcat(string $fieldName, string $alias, ?string $orderBy): string { return ", array_to_string(array_agg($fieldName ORDER BY $orderBy), ';') $alias "; } + #[\Override] public function close(): string { return ");"; } - public function integer(string $field, string $default = "", bool $pk = false, string $null = "NOT NULL"): string + #[\Override] + public function integer(string $field, string $default = "", bool $autoincrement = false, string $null = "NOT NULL"): string { - if ($pk) { + if ($autoincrement) { return "$field SERIAL PRIMARY KEY"; } return ", $field INTEGER $null $default"; } + #[\Override] public function string(string $field, string $default = "", string $null = "NOT NULL"): string { return ", $field VARCHAR(255) $null $default"; } + #[\Override] public function text(string $field, string $null = "NOT NULL"): string { return ", $field TEXT $null"; } - public function created_at(): string + #[\Override] + public function createdAt(): string { return ", created_at TIMESTAMP without time zone"; } - public function updated_at(): string + #[\Override] + public function updatedAt(): string { return ", updated_at TIMESTAMP without time zone"; } - public function pk(string $field): string + #[\Override] + public function primaryKey(string $field): string { return ""; } diff --git a/src/Driver/SQLite.php b/src/Drivers/SQLite.php similarity index 66% rename from src/Driver/SQLite.php rename to src/Drivers/SQLite.php index 4bce0ff..02c1cba 100755 --- a/src/Driver/SQLite.php +++ b/src/Drivers/SQLite.php @@ -9,50 +9,60 @@ * @license https://mozilla.org/MPL/2.0/ MPL-2.0 */ -namespace Rudra\Model\Driver; +namespace Rudra\Model\Drivers; -class SQLite +use Rudra\Model\Interfaces\SqlDialectInterface; + +class SQLite implements SqlDialectInterface { - public function concat(string $fieldName, string $alias, ?string $orderBy): string + #[\Override] + public function groupConcat(string $fieldName, string $alias, ?string $orderBy): string { return ", GROUP_CONCAT($fieldName,';') $alias "; } + #[\Override] public function close(): string { return ")"; } - public function integer(string $field, string $default = "", bool $pk = false, string $null = "NOT NULL"): string + #[\Override] + public function integer(string $field, string $default = "", bool $autoincrement = false, string $null = "NOT NULL"): string { - if ($pk) { + if ($autoincrement) { return "$field INTEGER PRIMARY KEY"; } return ", $field INTEGER $null $default"; } + #[\Override] public function string(string $field, string $default = "", string $null = "NOT NULL"): string { return ", $field TEXT $null $default"; } + #[\Override] public function text(string $field, string $null = "NOT NULL"): string { return ", $field TEXT $null"; } - public function created_at(): string + #[\Override] + public function createdAt(): string { return ", created_at TEXT DEFAULT CURRENT_TIMESTAMP"; } - public function updated_at(): string + #[\Override] + public function updatedAt(): string { return ", updated_at TEXT DEFAULT CURRENT_TIMESTAMP"; } - public function pk(string $field): string + #[\Override] + public function primaryKey(string $field): string { return ""; } diff --git a/src/Entity.php b/src/Entity.php index b6dd514..b7a8c29 100755 --- a/src/Entity.php +++ b/src/Entity.php @@ -11,7 +11,10 @@ namespace Rudra\Model; -class Entity +/** + * @mixin \Rudra\Model\Repository + */ +abstract class Entity { public static ?string $table = null; diff --git a/src/Interfaces/SqlDialectInterface.php b/src/Interfaces/SqlDialectInterface.php new file mode 100644 index 0000000..48d86ea --- /dev/null +++ b/src/Interfaces/SqlDialectInterface.php @@ -0,0 +1,24 @@ + + * @license https://mozilla.org/MPL/2.0/ MPL-2.0 + */ + +namespace Rudra\Model\Interfaces; + +interface SqlDialectInterface +{ + public function groupConcat(string $fieldName, string $alias, ?string $orderBy): string; + public function close(): string; + public function integer(string $field, string $default = "", bool $autoincrement = false, string $null = "NOT NULL"): string; + public function string(string $field, string $default = "", string $null = "NOT NULL"): string; + public function text(string $field, string $null = "NOT NULL"): string; + public function createdAt(): string; + public function updatedAt(): string; + public function primaryKey(string $field): string; +} diff --git a/src/Model.php b/src/Model.php index 5e2d6f2..92e2e79 100755 --- a/src/Model.php +++ b/src/Model.php @@ -13,7 +13,10 @@ use Rudra\Exceptions\RudraException; -class Model +/** + * @mixin \Rudra\Model\Repository + */ +abstract class Model { public ?string $table; diff --git a/src/QB.php b/src/QB.php index da7603b..75232b8 100755 --- a/src/QB.php +++ b/src/QB.php @@ -11,9 +11,9 @@ namespace Rudra\Model; -use Rudra\Model\Driver\MySQL; -use Rudra\Model\Driver\PgSQL; -use Rudra\Model\Driver\SQLite; +use Rudra\Model\Drivers\MySQL; +use Rudra\Model\Drivers\PgSQL; +use Rudra\Model\Drivers\SQLite; use Rudra\Container\Facades\Rudra; use Rudra\Exceptions\LogicException; @@ -49,9 +49,9 @@ public function select(string $fields = '*'): self return $this; } - public function concat(string $fieldName, string $alias, ?string $orderBy = null): self + public function groupConcat(string $fieldName, string $alias, ?string $orderBy = null): self { - $this->query .= $this->driver->concat($fieldName, $alias, $orderBy); + $this->query .= $this->driver->groupConcat($fieldName, $alias, $orderBy); return $this; } @@ -153,21 +153,21 @@ public function text(string $field, string $null = "NOT NULL"): self return $this; } - public function created_at(): self + public function createdAt(): self { - $this->query .= $this->driver->created_at(); + $this->query .= $this->driver->createdAt(); return $this; } - public function updated_at(): self + public function updatedAt(): self { - $this->query .= $this->driver->updated_at(); + $this->query .= $this->driver->updatedAt(); return $this; } - public function pk(?string $field = null): self + public function primaryKey(?string $field = null): self { - $this->query .= $this->driver->pk($field); + $this->query .= $this->driver->primaryKey($field); return $this; } } diff --git a/src/Repository.php b/src/Repository.php index 25be46d..2569679 100755 --- a/src/Repository.php +++ b/src/Repository.php @@ -11,16 +11,22 @@ namespace Rudra\Model; -use Rudra\Pagination; use Rudra\Container\Rudra; +use Rudra\Model\Traits\CrudTrait; +use Rudra\Model\Traits\CacheTrait; +use Rudra\Model\Traits\SchemaTrait; use Rudra\Exceptions\LogicException; class Repository { + use CrudTrait; + use CacheTrait; + use SchemaTrait; + public ?string $table; private Rudra $rudra; - private \PDO $connection; - private QB $qb; + protected \PDO $connection; + protected QB $qb; /** * Initializes the class with a table name, connection, and sets up dependencies. @@ -107,305 +113,4 @@ public function qBuilder(string $queryString, array $queryParams = []): array return $stmt->fetchAll(\PDO::FETCH_ASSOC); } - - public function getAllPerPage(Pagination $pagination, ?string $fields = null): array - { - $fields = !isset($fields) ? implode(',', $this->getFields($fields)) : $fields; - $qString = $this->qb()->select($fields) - ->from($this->table) - ->orderBy("id DESC") - ->limit($pagination->getPerPage())->offset($pagination->getOffset()) - ->get(); - - return $this->qBuilder($qString); - } - - public function find(int|string $id): array|false - { - $stmt = $this->connection->prepare(" - SELECT * FROM {$this->table} - WHERE id = :id - "); - - $stmt->execute([ - ':id' => $id, - ]); - - return $stmt->fetch(\PDO::FETCH_ASSOC); - } - - public function getAll(string $sort = 'id ASC', ?string $fields = null): array - { - $fields = !isset($fields) ? implode(',', $this->getFields($fields)) : $fields; - $table = $this->table; - $qString = $this->qb()->select($fields) - ->from($table) - ->orderBy($sort) - ->get(); - - return $this->qBuilder($qString); - } - - public function numRows(): int - { - $table = $this->table; - $count = $this->connection->query("SELECT COUNT(*) FROM {$table}"); - - return (int)$count->fetchColumn(); - } - - /** - * Finds a single record by a specified field and value. - * The field name is validated against the actual table columns to prevent SQL injection. - * - * @throws LogicException if the field is not a valid column name - */ - public function findBy(string $field, mixed $value): array|false - { - $table = $this->table; - $stmt = $this->connection->prepare(" - SELECT * FROM {$table} - WHERE {$field} = :val - "); - - $stmt->execute([ - ":val" => $value, - ]); - - return $stmt->fetch(\PDO::FETCH_ASSOC); - } - - public function lastInsertId(): string - { - return $this->connection->lastInsertId(); - } - - public function update(array $fields): void - { - $id = $fields['id']; - unset($fields['id']); - $stmtString = $this->updateStmtString($fields); - $fields['id'] = $id; - - $query = $this->connection->prepare(" - UPDATE {$this->table} SET {$stmtString} - WHERE id =:id"); - - $query->execute($fields); - $this->clearCache(); - } - - public function create(array $fields): void - { - $table = $this->table; - $stmtString = $this->createStmtString($fields); - - $query = $this->connection->prepare(" - INSERT INTO {$table} ({$stmtString[0]}) - VALUES ({$stmtString[1]})"); - - $query->execute($fields); - $this->clearCache(); - } - - public function delete(int|string $id): void - { - $table = $this->table; - $query = $this->connection->prepare("DELETE FROM {$table} WHERE id = :id"); - $query->execute([':id' => $id]); - $this->clearCache(); - } - - /** - * Generates a string of fields and placeholders for an SQL UPDATE statement. - * The method takes an array of fields and constructs a comma-separated list of "key=:key" pairs. - * This string can be directly used in the SET clause of an SQL UPDATE query. - */ - protected static function updateStmtString(array $fields): string - { - $stmtFields = []; - - foreach (array_keys($fields) as $key) { - $stmtFields[] = "{$key}=:{$key}"; - } - - return implode(",", $stmtFields); - } - - /** - * Generates two strings for an SQL INSERT statement: one for column names and one for placeholders. - * The method takes an array of fields and constructs two comma-separated lists: - * - A list of column names. - * - A list of placeholders (prefixed with colons) for parameter binding. - * These strings can be directly used in the SQL INSERT query. - */ - protected static function createStmtString(array $fields): array - { - $insert = []; - $execute = []; - - foreach (array_keys($fields) as $key) { - $insert[] = $key; - $execute[] = ":{$key}"; - } - - return [implode(",", $insert), implode(",", $execute)]; - } - - /** - * Retrieves the column information for the current table based on the database driver. - * The method executes a query specific to the database type (MySQL, PostgreSQL, or SQLite) - * to fetch the column details of the table. - * - * @throws \PDOException - */ - public function getColumns(): array - { - $table = $this->table; - - if ($this->connection->getAttribute(\PDO::ATTR_DRIVER_NAME) === "mysql") { - $query = $this->connection->query("SHOW COLUMNS FROM {$table}"); - } elseif ($this->connection->getAttribute(\PDO::ATTR_DRIVER_NAME) === "pgsql") { - $query = $this->connection->query("SELECT column_name, data_type - FROM information_schema.columns - WHERE table_name = '{$table}'" - ); - } elseif ($this->connection->getAttribute(\PDO::ATTR_DRIVER_NAME) === "sqlite") { - $query = $this->connection->query("PRAGMA table_info('{$table}')" - ); - } - - return $query->fetchAll(\PDO::FETCH_ASSOC); - } - - /** - * Retrieves the list of fields (columns) for the current table. - * If no specific fields are provided, the method fetches all column names based on the database driver. - * Otherwise, it splits the provided comma-separated string of fields into an array. - */ - public function getFields(?string $fields = null): array - { - if ($fields !== null) { - // Split by comma and remove spaces around each field - return array_map('trim', explode(',', $fields)); - } - - // Initialize as an empty array — protection against null - $fieldList = []; - - if ($this->connection->getAttribute(\PDO::ATTR_DRIVER_NAME) === "mysql") { - foreach ($this->getColumns() as $column) { - $fieldList[] = $column['Field']; - } - } elseif ($this->connection->getAttribute(\PDO::ATTR_DRIVER_NAME) === "pgsql") { - foreach ($this->getColumns() as $column) { - $fieldList[] = $column['column_name']; - } - } elseif ($this->connection->getAttribute(\PDO::ATTR_DRIVER_NAME) === "sqlite") { - foreach ($this->getColumns() as $column) { - $fieldList[] = $column['name']; - } - } - - return $fieldList; - } - - /** - * Searches for records in the database based on a search term and column. - * The column name is validated against the actual table columns to prevent SQL injection. - * Results are ordered by ID in descending order and limited to 10 records. - * - * @throws LogicException if the column is not a valid column name - */ - public function search(string $search, string $column, ?string $fields = null): array - { - $table = $this->table; - $fields = $fields ?: implode(',', $this->getFields()); - $driver = $this->connection->getAttribute(\PDO::ATTR_DRIVER_NAME); - - // Form an expression for casting to string - $searchExpr = match ($driver) { - 'pgsql' => "$column::TEXT", // PostgreSQL - 'mysql' => "CAST($column AS CHAR)", // MySQL - 'sqlite' => "CAST($column AS TEXT)", // SQLite - default => "$column", // fallback (If suddenly another DBMS) - }; - - $query = $this->connection->prepare(" - SELECT {$fields} FROM {$table} - WHERE {$searchExpr} LIKE :search - ORDER BY id DESC - LIMIT 10 - "); - - $query->execute([':search' => "%{$search}%"]); - return $query->fetchAll(\PDO::FETCH_ASSOC); - } - - /** - * Caches the result of a method call to a JSON file for a specified duration. - * If the cached file exists and is still valid (based on cache time), the cached data is returned. - * Otherwise, the method executes the specified method, caches its result, and returns the data. - */ - public function cache(array $params, ?string $cacheTime = null): mixed - { - $directory = dirname(__DIR__, 4) . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'database'; - $file = "$directory/$params[0].json"; - $cacheTime = $cacheTime ?? config('cache.time', 'database'); - - if (!is_dir($directory)) { - mkdir($directory, 0755, true); - } - - if (file_exists($file) && (strtotime($cacheTime, filemtime($file)) > time())) { - return json_decode(file_get_contents($file), true); - } - - $method = (strpos($params[0], '_') !== false) ? strstr($params[0], '_', true) : $params[0]; - $data = (!array_key_exists(1, $params)) ? $this->$method() : $this->$method(...$params[1]); - file_put_contents($file, json_encode($data, JSON_UNESCAPED_UNICODE)); - - return $data; - } - - /** - * Clears cached files of a specified type or all types. - * If a cache key is provided, only that specific cache file is deleted. - */ - public function clearCache(string $type = 'database', ?string $key = null): void - { - $baseDir = dirname(__DIR__, 4) . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR; - - if ($type === 'all') { - $this->clearCache('database', $key); - $this->clearCache('templates', $key); - $this->clearCache('twig', $key); - $this->clearCache('routes', $key); - return; - } - - if (!in_array($type, ['database', 'templates', 'twig', 'routes'], true)) { - return; - } - - $directory = $baseDir . $type; - - if ($key !== null) { - // Delete one file - $file = $directory . DIRECTORY_SEPARATOR . $key . '.json'; - if (is_file($file)) { - unlink($file); - } - return; - } - - // Delete all files in the directory - if (is_dir($directory)) { - foreach (glob("$directory/*.json") as $file) { - if (is_file($file)) { - unlink($file); - } - } - } - } } diff --git a/src/Traits/CacheTrait.php b/src/Traits/CacheTrait.php new file mode 100644 index 0000000..2cd1877 --- /dev/null +++ b/src/Traits/CacheTrait.php @@ -0,0 +1,82 @@ + + * @license https://mozilla.org/MPL/2.0/ MPL-2.0 + */ + +namespace Rudra\Model\Traits; + +trait CacheTrait +{ + /** + * Caches the result of a method call to a JSON file for a specified duration. + * If the cached file exists and is still valid (based on cache time), the cached data is returned. + * Otherwise, the method executes the specified method, caches its result, and returns the data. + */ + public function cache(array $params, ?string $cacheTime = null): mixed + { + $directory = dirname(__DIR__, 4) . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'database'; + $file = "$directory/$params[0].json"; + $cacheTime = $cacheTime ?? config('cache.time', 'database'); + + if (!is_dir($directory)) { + mkdir($directory, 0755, true); + } + + if (file_exists($file) && (strtotime($cacheTime, filemtime($file)) > time())) { + return json_decode(file_get_contents($file), true); + } + + $method = (strpos($params[0], '_') !== false) ? strstr($params[0], '_', true) : $params[0]; + $data = (!array_key_exists(1, $params)) ? $this->$method() : $this->$method(...$params[1]); + file_put_contents($file, json_encode($data, JSON_UNESCAPED_UNICODE)); + + return $data; + } + + /** + * Clears cached files of a specified type or all types. + * If a cache key is provided, only that specific cache file is deleted. + */ + public function clearCache(string $type = 'database', ?string $key = null): void + { + $baseDir = dirname(__DIR__, 4) . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR; + + if ($type === 'all') { + $this->clearCache('database', $key); + $this->clearCache('templates', $key); + $this->clearCache('twig', $key); + $this->clearCache('routes', $key); + return; + } + + if (!in_array($type, ['database', 'templates', 'twig', 'routes'], true)) { + return; + } + + $directory = $baseDir . $type; + + if ($key !== null) { + // Delete one file + $file = $directory . DIRECTORY_SEPARATOR . $key . '.json'; + if (is_file($file)) { + unlink($file); + } + return; + } + + // Delete all files in the directory + if (is_dir($directory)) { + foreach (glob("$directory/*.json") as $file) { + if (is_file($file)) { + unlink($file); + } + } + } + } +} diff --git a/src/Traits/CrudTrait.php b/src/Traits/CrudTrait.php new file mode 100644 index 0000000..6031386 --- /dev/null +++ b/src/Traits/CrudTrait.php @@ -0,0 +1,194 @@ + + * @license https://mozilla.org/MPL/2.0/ MPL-2.0 + */ + +namespace Rudra\Model\Traits; + +use Rudra\Pagination; + +trait CrudTrait +{ + public function getAllPerPage(Pagination $pagination, ?string $fields = null): array + { + $fields = $fields ?? implode(',', $this->getFields()); + $qString = $this->qb()->select($fields) + ->from($this->table) + ->orderBy("id DESC") + ->limit($pagination->getPerPage())->offset($pagination->getOffset()) + ->get(); + + return $this->qBuilder($qString); + } + + public function getAll(string $sort = 'id ASC', ?string $fields = null): array + { + $fields = $fields ?? implode(',', $this->getFields()); + $table = $this->table; + $qString = $this->qb()->select($fields) + ->from($table) + ->orderBy($sort) + ->get(); + + return $this->qBuilder($qString); + } + + public function numRows(): int + { + $table = $this->table; + $count = $this->connection->query("SELECT COUNT(*) FROM {$table}"); + + return (int)$count->fetchColumn(); + } + + /** + * Finds a single record by a specified field and value. + * + * WARNING: The $field parameter is inserted directly into the SQL query. + * It is the developer's responsibility to ensure the field name is valid + * and sanitized to prevent SQL injection. Do not pass raw user input here. + */ + public function findBy(string $field, mixed $value): array|false + { + $table = $this->table; + $stmt = $this->connection->prepare(" + SELECT * FROM {$table} + WHERE {$field} = :val + "); + + $stmt->execute([ + ":val" => $value, + ]); + + return $stmt->fetch(\PDO::FETCH_ASSOC); + } + + public function lastInsertId(): string + { + return $this->connection->lastInsertId(); + } + + /** + * Searches for records in the database based on a search term and column. + * + * WARNING: The $column parameter is inserted directly into the SQL query. + * Ensure it is sanitized to prevent SQL injection. + * Results are ordered by ID in descending order and limited to 10 records. + */ + public function search(string $search, string $column, ?string $fields = null): array + { + $table = $this->table; + $fields = $fields ?: implode(',', $this->getFields()); + $driver = $this->connection->getAttribute(\PDO::ATTR_DRIVER_NAME); + + // Form an expression for casting to string + $searchExpr = match ($driver) { + 'pgsql' => "$column::TEXT", // PostgreSQL + 'mysql' => "CAST($column AS CHAR)", // MySQL + 'sqlite' => "CAST($column AS TEXT)", // SQLite + default => "$column", // fallback (If suddenly another DBMS) + }; + + $query = $this->connection->prepare(" + SELECT {$fields} FROM {$table} + WHERE {$searchExpr} LIKE :search + ORDER BY id DESC + LIMIT 10 + "); + + $query->execute([':search' => "%{$search}%"]); + return $query->fetchAll(\PDO::FETCH_ASSOC); + } + + public function find(int|string $id): array|false + { + $stmt = $this->connection->prepare(" + SELECT * FROM {$this->table} + WHERE id = :id + "); + + $stmt->execute([ + ':id' => $id, + ]); + + return $stmt->fetch(\PDO::FETCH_ASSOC); + } + + public function update(array $fields): void + { + $id = $fields['id']; + unset($fields['id']); + $stmtString = $this->updateStmtString($fields); + $fields['id'] = $id; + + $query = $this->connection->prepare(" + UPDATE {$this->table} SET {$stmtString} + WHERE id =:id"); + + $query->execute($fields); + $this->clearCache(); + } + + public function create(array $fields): void + { + $table = $this->table; + $stmtString = $this->createStmtString($fields); + + $query = $this->connection->prepare(" + INSERT INTO {$table} ({$stmtString[0]}) + VALUES ({$stmtString[1]})"); + + $query->execute($fields); + $this->clearCache(); + } + + public function delete(int|string $id): void + { + $table = $this->table; + $query = $this->connection->prepare("DELETE FROM {$table} WHERE id = :id"); + $query->execute([':id' => $id]); + $this->clearCache(); + } + + /** + * Generates a string of fields and placeholders for an SQL UPDATE statement. + * The method takes an array of fields and constructs a comma-separated list of "key=:key" pairs. + * This string can be directly used in the SET clause of an SQL UPDATE query. + */ + protected static function updateStmtString(array $fields): string + { + $stmtFields = []; + + foreach (array_keys($fields) as $key) { + $stmtFields[] = "{$key}=:{$key}"; + } + + return implode(",", $stmtFields); + } + + /** + * Generates two strings for an SQL INSERT statement: one for column names and one for placeholders. + * The method takes an array of fields and constructs two comma-separated lists: + * - A list of column names. + * - A list of placeholders (prefixed with colons) for parameter binding. + * These strings can be directly used in the SQL INSERT query. + */ + protected static function createStmtString(array $fields): array + { + $insert = []; + $execute = []; + + foreach (array_keys($fields) as $key) { + $insert[] = $key; + $execute[] = ":{$key}"; + } + + return [implode(",", $insert), implode(",", $execute)]; + } +} diff --git a/src/Traits/SchemaTrait.php b/src/Traits/SchemaTrait.php new file mode 100644 index 0000000..38f36e5 --- /dev/null +++ b/src/Traits/SchemaTrait.php @@ -0,0 +1,73 @@ + + * @license https://mozilla.org/MPL/2.0/ MPL-2.0 + */ + +namespace Rudra\Model\Traits; + +trait SchemaTrait +{ + /** + * Retrieves the column information for the current table based on the database driver. + * The method executes a query specific to the database type (MySQL, PostgreSQL, or SQLite) + * to fetch the column details of the table. + * + * @throws \PDOException + */ + public function getColumns(): array + { + $table = $this->table; + + if ($this->connection->getAttribute(\PDO::ATTR_DRIVER_NAME) === "mysql") { + $query = $this->connection->query("SHOW COLUMNS FROM {$table}"); + } elseif ($this->connection->getAttribute(\PDO::ATTR_DRIVER_NAME) === "pgsql") { + $query = $this->connection->query("SELECT column_name, data_type + FROM information_schema.columns + WHERE table_name = '{$table}'" + ); + } elseif ($this->connection->getAttribute(\PDO::ATTR_DRIVER_NAME) === "sqlite") { + $query = $this->connection->query("PRAGMA table_info('{$table}')" + ); + } + + return $query->fetchAll(\PDO::FETCH_ASSOC); + } + + /** + * Retrieves the list of fields (columns) for the current table. + * If no specific fields are provided, the method fetches all column names based on the database driver. + * Otherwise, it splits the provided comma-separated string of fields into an array. + */ + public function getFields(?string $fields = null): array + { + if ($fields !== null) { + // Split by comma and remove spaces around each field + return array_map('trim', explode(',', $fields)); + } + + // Initialize as an empty array — protection against null + $fieldList = []; + + if ($this->connection->getAttribute(\PDO::ATTR_DRIVER_NAME) === "mysql") { + foreach ($this->getColumns() as $column) { + $fieldList[] = $column['Field']; + } + } elseif ($this->connection->getAttribute(\PDO::ATTR_DRIVER_NAME) === "pgsql") { + foreach ($this->getColumns() as $column) { + $fieldList[] = $column['column_name']; + } + } elseif ($this->connection->getAttribute(\PDO::ATTR_DRIVER_NAME) === "sqlite") { + foreach ($this->getColumns() as $column) { + $fieldList[] = $column['name']; + } + } + + return $fieldList; + } +} diff --git a/tests/RepositoryTest.php b/tests/RepositoryTest.php index c3d682a..3dd9aa7 100755 --- a/tests/RepositoryTest.php +++ b/tests/RepositoryTest.php @@ -20,7 +20,6 @@ class RepositoryTest extends TestCase { protected function setUp(): void { - } public function testGetAllReturnsExpectedData()