From 59ab9795b3ee6d02b2e12aa0d88a2f6cc023e7b4 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Sun, 25 Jan 2026 16:04:55 +0400 Subject: [PATCH 1/4] Add Optional Timestamps Support to Models --- CHANGELOG.md | 7 + src/Model/DbModel.php | 4 + src/Model/Helpers/model.php | 2 +- src/Model/Traits/HasTimestamps.php | 126 ++++++++++++ src/Model/Traits/SoftDeletes.php | 24 ++- .../src/Controllers/AccountController.php.tpl | 12 +- .../src/Controllers/CommentController.php.tpl | 9 +- .../src/Controllers/PostController.php.tpl | 13 +- .../PostManagementController.php.tpl | 14 +- .../DemoApi/src/Models/Comment.php.tpl | 4 +- .../Templates/DemoApi/src/Models/Post.php.tpl | 4 +- .../Templates/DemoApi/src/Models/User.php.tpl | 5 +- .../DemoApi/src/Services/AuthService.php.tpl | 9 +- .../src/Services/CommentService.php.tpl | 3 +- .../DemoApi/src/Services/PostService.php.tpl | 7 +- .../src/Controllers/AccountController.php.tpl | 4 +- .../src/Controllers/AuthController.php.tpl | 16 +- .../src/Controllers/BaseController.php.tpl | 4 +- .../src/Controllers/CommentController.php.tpl | 4 +- .../src/Controllers/PageController.php.tpl | 4 +- .../src/Controllers/PostController.php.tpl | 10 +- .../PostManagementController.php.tpl | 7 +- .../DemoWeb/src/Models/Comment.php.tpl | 4 +- .../Templates/DemoWeb/src/Models/Post.php.tpl | 4 +- .../Templates/DemoWeb/src/Models/User.php.tpl | 5 +- .../DemoWeb/src/Services/AuthService.php.tpl | 9 +- .../src/Services/CommentService.php.tpl | 4 +- .../DemoWeb/src/Services/PostService.php.tpl | 3 - .../src/Controllers/BaseController.php.tpl | 13 +- .../Controllers/DashboardController.php.tpl | 13 +- .../Controllers/DatabaseController.php.tpl | 7 +- .../src/Controllers/EmailsController.php.tpl | 13 +- .../src/Controllers/LogsController.php.tpl | 19 +- .../src/Middlewares/BaseMiddleware.php.tpl | 7 +- .../Toolkit/src/Middlewares/BasicAuth.php.tpl | 5 +- .../src/Middlewares/CreateTable.php.tpl | 5 +- .../src/Services/DashboardService.php.tpl | 5 +- .../src/Services/DatabaseService.php.tpl | 7 +- .../Toolkit/src/Services/EmailService.php.tpl | 7 +- .../Toolkit/src/Services/LogsService.php.tpl | 5 +- src/Service/Helpers/service.php | 8 +- tests/Unit/Model/DbModelTimestampsTest.php | 184 ++++++++++++++++++ .../Models/TestPostCustomTimestampModel.php | 24 +++ .../shared/Models/TestPostTimestampModel.php | 22 +++ .../Models/TestPostUnixTimestampModel.php | 24 +++ 45 files changed, 529 insertions(+), 160 deletions(-) create mode 100644 src/Model/Traits/HasTimestamps.php create mode 100644 tests/Unit/Model/DbModelTimestampsTest.php create mode 100644 tests/_root/shared/Models/TestPostCustomTimestampModel.php create mode 100644 tests/_root/shared/Models/TestPostTimestampModel.php create mode 100644 tests/_root/shared/Models/TestPostUnixTimestampModel.php diff --git a/CHANGELOG.md b/CHANGELOG.md index ce201644..194c1a52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Calling create() resets model state to ensure clean inserts - Database-generated primary keys are now synced into the model after save() - Models are hydrated only on fetch operations, not implicitly via getters +- `DbModel::save()` now automatically applies timestamps when the model uses the `HasTimestamps` trait +- `SoftDeletes` now uses model timestamp format when available (datetime/unix) for `deleted_at` ### Fixed - PHP 8.1 compatibility: Fixed null parameter deprecations in `explode()`, `parse_url()`, and `str_replace()` @@ -48,6 +50,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for force mode and specific task execution - Automatic cleanup of stale locks (older than 24 hours) - Full documentation in `docs/cron-scheduler.md` +- **Opt-in Model Timestamps**: Introduced `HasTimestamps` trait for `DbModel`: + - Automatically sets `created_at` on insert + - Automatically sets `updated_at` on insert and update + - Supports custom timestamp column names via model constants (`CREATED_AT`, `UPDATED_AT`) + - Supports datetime and unix timestamp formats via `TIMESTAMP_TYPE` ### Removed - Support for PHP 7.3 and earlier versions diff --git a/src/Model/DbModel.php b/src/Model/DbModel.php index 5b7501cc..d5965231 100644 --- a/src/Model/DbModel.php +++ b/src/Model/DbModel.php @@ -182,6 +182,10 @@ public function create(): self */ public function save(): bool { + if (method_exists($this, 'touchTimestamps')) { + $this->touchTimestamps(); + } + $this->syncAttributesToOrm(); $result = $this->getOrmInstance()->save(); diff --git a/src/Model/Helpers/model.php b/src/Model/Helpers/model.php index 9395afdc..29954e80 100644 --- a/src/Model/Helpers/model.php +++ b/src/Model/Helpers/model.php @@ -20,10 +20,10 @@ /** * Gets the model instance - * @template T of Model * @param class-string $modelClass * @return T * @throws ModelException + * @template T of Model */ function model(string $modelClass): Model { diff --git a/src/Model/Traits/HasTimestamps.php b/src/Model/Traits/HasTimestamps.php new file mode 100644 index 00000000..9c82cf3a --- /dev/null +++ b/src/Model/Traits/HasTimestamps.php @@ -0,0 +1,126 @@ + + * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) + * @link http://quantum.softberg.org/ + * @since 3.0.0 + */ + +namespace Quantum\Model\Traits; + +/** + * Trait HasTimestamps + * + * @property array $attributes + * @property string $idColumn + */ +trait HasTimestamps +{ + /** + * Column name for created timestamp + * @var string + */ + public string $createdAt = 'created_at'; + + /** + * Column name for updated timestamp + * @var string + */ + public string $updatedAt = 'updated_at'; + + /** + * Timestamp storage type: datetime|unix + * @var string + */ + public string $timestampType = 'datetime'; + + /** + * Determine whether the current model is new (insert) or existing (update). + * @return bool + */ + protected function isNewRecord(): bool + { + $id = $this->attributes[$this->idColumn] ?? null; + + return empty($id); + } + + /** + * Returns the current timestamp value based on model config. + * Supports: datetime|unix + * @return int|string + */ + protected function nowTimestampValue() + { + if ($this->getTimestampType() === 'unix') { + return time(); + } + + return date('Y-m-d H:i:s'); + } + + /** + * Get timestamp type. + * @return string + */ + protected function getTimestampType(): string + { + if (defined(static::class . '::TIMESTAMP_TYPE')) { + return static::TIMESTAMP_TYPE; + } + + return $this->timestampType; + } + + /** + * Get "created at" column name. + * @return string + */ + protected function getCreatedAtColumn(): string + { + if (defined(static::class . '::CREATED_AT')) { + return static::CREATED_AT; + } + + return 'created_at'; + } + + /** + * Get "updated at" column name. + * @return string + */ + protected function getUpdatedAtColumn(): string + { + if (defined(static::class . '::UPDATED_AT')) { + return static::UPDATED_AT; + } + + return 'updated_at'; + } + + /** + * Touch timestamps on save + * @return void + */ + protected function touchTimestamps(): void + { + $now = $this->nowTimestampValue(); + + $createdAt = $this->getCreatedAtColumn(); + $updatedAt = $this->getUpdatedAtColumn(); + + if ($this->isNewRecord()) { + if (!array_key_exists($createdAt, $this->attributes)) { + $this->attributes[$createdAt] = $now; + } + } + + $this->attributes[$updatedAt] = $now; + } +} diff --git a/src/Model/Traits/SoftDeletes.php b/src/Model/Traits/SoftDeletes.php index 3342d3e7..82cb39cd 100644 --- a/src/Model/Traits/SoftDeletes.php +++ b/src/Model/Traits/SoftDeletes.php @@ -39,9 +39,8 @@ trait SoftDeletes */ public function delete(): bool { - $this->prop($this->getDeleteAtColumn(), date('Y-m-d H:i:s')); + $this->prop($this->getDeletedAtColumn(), $this->getSoftDeleteTimestampValue()); return $this->save(); - } /** @@ -51,7 +50,7 @@ public function delete(): bool */ public function restore(): bool { - $this->prop($this->getDeleteAtColumn(), null); + $this->prop($this->getDeletedAtColumn(), null); return $this->save(); } @@ -85,7 +84,7 @@ public function onlyTrashed(): self { $this->includeTrashed = true; - $this->getOrmInstance()->isNotNull($this->getDeleteAtColumn()); + $this->getOrmInstance()->isNotNull($this->getDeletedAtColumn()); return $this; } @@ -175,15 +174,28 @@ public function first(): ?DbModel protected function applySoftDeleteScope(): void { if (!$this->includeTrashed) { - $this->getOrmInstance()->isNull($this->getDeleteAtColumn()); + $this->getOrmInstance()->isNull($this->getDeletedAtColumn()); + } + } + + /** + * Returns timestamp value for soft delete based on model timestamp config (if available). + * @return int|string + */ + protected function getSoftDeleteTimestampValue() + { + if (method_exists($this, 'nowTimestampValue')) { + return $this->nowTimestampValue(); } + + return date('Y-m-d H:i:s'); } /** * Get the column name used for soft deletes. * @return string */ - protected function getDeleteAtColumn(): string + protected function getDeletedAtColumn(): string { if (defined(static::class . '::DELETED_AT')) { return static::DELETED_AT; diff --git a/src/Module/Templates/DemoApi/src/Controllers/AccountController.php.tpl b/src/Module/Templates/DemoApi/src/Controllers/AccountController.php.tpl index fe92f35d..26134c4b 100644 --- a/src/Module/Templates/DemoApi/src/Controllers/AccountController.php.tpl +++ b/src/Module/Templates/DemoApi/src/Controllers/AccountController.php.tpl @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.9 + * @since 3.0.0 */ namespace {{MODULE_NAMESPACE}}\Controllers; @@ -30,11 +30,19 @@ class AccountController extends BaseController * Auth service * @var AuthService */ - public $authService; + public AuthService $authService; /** * Works before an action */ + + /** + * @return void + * @throws ReflectionException + * @throws \Quantum\App\Exceptions\BaseException + * @throws \Quantum\Di\Exceptions\DiException + * @throws \Quantum\Service\Exceptions\ServiceException + */ public function __before() { $this->authService = service(AuthService::class); diff --git a/src/Module/Templates/DemoApi/src/Controllers/CommentController.php.tpl b/src/Module/Templates/DemoApi/src/Controllers/CommentController.php.tpl index ec0af3c5..54beb1c8 100644 --- a/src/Module/Templates/DemoApi/src/Controllers/CommentController.php.tpl +++ b/src/Module/Templates/DemoApi/src/Controllers/CommentController.php.tpl @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.9 + * @since 3.0.0 */ namespace {{MODULE_NAMESPACE}}\Controllers; @@ -31,13 +31,10 @@ class CommentController extends BaseController /** * @var CommentService */ - public $commentService; + public CommentService $commentService; /** - * @throws ReflectionException - * @throws BaseException - * @throws DiException - * @throws ServiceException + * Works before an action */ public function __before() { diff --git a/src/Module/Templates/DemoApi/src/Controllers/PostController.php.tpl b/src/Module/Templates/DemoApi/src/Controllers/PostController.php.tpl index 1607bc59..d8174ae3 100644 --- a/src/Module/Templates/DemoApi/src/Controllers/PostController.php.tpl +++ b/src/Module/Templates/DemoApi/src/Controllers/PostController.php.tpl @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.9 + * @since 3.0.0 */ namespace {{MODULE_NAMESPACE}}\Controllers; @@ -32,24 +32,21 @@ class PostController extends BaseController /** * Posts per page */ - const POSTS_PER_PAGE = 8; + protected const POSTS_PER_PAGE = 8; /** * Current page */ - const CURRENT_PAGE = 1; + protected const CURRENT_PAGE = 1; /** * Post service * @var PostService */ - public $postService; + public PostService $postService; /** - * @throws ReflectionException - * @throws BaseException - * @throws DiException - * @throws ServiceException + * Works before an action */ public function __before() { diff --git a/src/Module/Templates/DemoApi/src/Controllers/PostManagementController.php.tpl b/src/Module/Templates/DemoApi/src/Controllers/PostManagementController.php.tpl index d642d2ff..c9e34333 100644 --- a/src/Module/Templates/DemoApi/src/Controllers/PostManagementController.php.tpl +++ b/src/Module/Templates/DemoApi/src/Controllers/PostManagementController.php.tpl @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.9 + * @since 3.0.0 */ namespace {{MODULE_NAMESPACE}}\Controllers; @@ -32,18 +32,11 @@ class PostManagementController extends BaseController * Post service * @var PostService */ - public $postService; + public PostService $postService; /** * Works before an action */ - - /** - * @throws ReflectionException - * @throws BaseException - * @throws DiException - * @throws ServiceException - */ public function __before() { $this->postService = service(PostService::class); @@ -75,7 +68,6 @@ class PostManagementController extends BaseController 'title' => $request->get('title', null, true), 'content' => $request->get('content', null, true), 'image' => '', - 'updated_at' => date('Y-m-d H:i:s'), ]; if ($request->hasFile('image')) { @@ -109,7 +101,6 @@ class PostManagementController extends BaseController $postData = [ 'title' => $request->get('title', null, true), 'content' => $request->get('content', null, true), - 'updated_at' => date('Y-m-d H:i:s'), ]; $post = $this->postService->getPost($postUuid); @@ -177,7 +168,6 @@ class PostManagementController extends BaseController 'title' => $post->title, 'content' => $post->content, 'image' => '', - 'updated_at' => date('Y-m-d H:i:s'), ]); $response->json([ diff --git a/src/Module/Templates/DemoApi/src/Models/Comment.php.tpl b/src/Module/Templates/DemoApi/src/Models/Comment.php.tpl index 1027c383..6a4911e4 100644 --- a/src/Module/Templates/DemoApi/src/Models/Comment.php.tpl +++ b/src/Module/Templates/DemoApi/src/Models/Comment.php.tpl @@ -15,6 +15,7 @@ namespace {{MODULE_NAMESPACE}}\Models; use Quantum\Libraries\Database\Enums\Relation; +use Quantum\Model\Traits\HasTimestamps; use Quantum\Model\Traits\SoftDeletes; use Quantum\Model\DbModel; @@ -25,6 +26,7 @@ use Quantum\Model\DbModel; class Comment extends DbModel { + use HasTimestamps; use SoftDeletes; /** @@ -48,8 +50,6 @@ class Comment extends DbModel 'post_uuid', 'user_uuid', 'content', - 'created_at', - 'updated_at' ]; /** diff --git a/src/Module/Templates/DemoApi/src/Models/Post.php.tpl b/src/Module/Templates/DemoApi/src/Models/Post.php.tpl index fe0537fa..9f7ed5f0 100644 --- a/src/Module/Templates/DemoApi/src/Models/Post.php.tpl +++ b/src/Module/Templates/DemoApi/src/Models/Post.php.tpl @@ -15,6 +15,7 @@ namespace {{MODULE_NAMESPACE}}\Models; use Quantum\Libraries\Database\Enums\Relation; +use Quantum\Model\Traits\HasTimestamps; use Quantum\Model\Traits\SoftDeletes; use Quantum\Model\DbModel; @@ -25,6 +26,7 @@ use Quantum\Model\DbModel; class Post extends DbModel { + use HasTimestamps; use SoftDeletes; /** @@ -49,8 +51,6 @@ class Post extends DbModel 'title', 'content', 'image', - 'created_at', - 'updated_at', ]; /** diff --git a/src/Module/Templates/DemoApi/src/Models/User.php.tpl b/src/Module/Templates/DemoApi/src/Models/User.php.tpl index b4331053..d80e57d6 100644 --- a/src/Module/Templates/DemoApi/src/Models/User.php.tpl +++ b/src/Module/Templates/DemoApi/src/Models/User.php.tpl @@ -14,6 +14,7 @@ namespace {{MODULE_NAMESPACE}}\Models; +use Quantum\Model\Traits\HasTimestamps; use Quantum\Model\DbModel; /** @@ -23,6 +24,8 @@ use Quantum\Model\DbModel; class User extends DbModel { + use HasTimestamps; + /** * ID column of table * @var string @@ -55,7 +58,5 @@ class User extends DbModel 'otp', 'otp_expires', 'otp_token', - 'created_at', - 'updated_at', ]; } \ No newline at end of file diff --git a/src/Module/Templates/DemoApi/src/Services/AuthService.php.tpl b/src/Module/Templates/DemoApi/src/Services/AuthService.php.tpl index 4af8dd04..5a0d8556 100644 --- a/src/Module/Templates/DemoApi/src/Services/AuthService.php.tpl +++ b/src/Module/Templates/DemoApi/src/Services/AuthService.php.tpl @@ -57,12 +57,12 @@ class AuthService extends QtService implements AuthServiceInterface /** * Get user * @param string $uuid - * @return User + * @return User|null * @throws BaseException */ - public function getUserByUuid(string $uuid): User + public function getUserByUuid(string $uuid): ?User { - return$this->model->findOneBy('uuid', $uuid); + return $this->model->findOneBy('uuid', $uuid); } /** @@ -95,7 +95,6 @@ class AuthService extends QtService implements AuthServiceInterface public function add(array $data): AuthUser { $data['uuid'] = $data['uuid'] ?? uuid_ordered(); - $data['created_at'] = date('Y-m-d H:i:s'); $this->createUserDirectory($data['uuid']); @@ -123,8 +122,6 @@ class AuthService extends QtService implements AuthServiceInterface return null; } - $data['updated_at'] = date('Y-m-d H:i:s'); - $user->fill($data); $user->save(); diff --git a/src/Module/Templates/DemoApi/src/Services/CommentService.php.tpl b/src/Module/Templates/DemoApi/src/Services/CommentService.php.tpl index c93291d7..1efd63e1 100644 --- a/src/Module/Templates/DemoApi/src/Services/CommentService.php.tpl +++ b/src/Module/Templates/DemoApi/src/Services/CommentService.php.tpl @@ -93,7 +93,6 @@ class CommentService extends QtService public function addComment(array $data): array { $data['uuid'] = $data['uuid'] ?? uuid_ordered(); - $data['created_at'] = date('Y-m-d H:i:s'); $comment = $this->model->create(); $comment->fill($data); @@ -106,6 +105,8 @@ class CommentService extends QtService * Delete a comment * @param string $uuid * @return bool + * @throws BaseException + * @throws ModelException */ public function deleteComment(string $uuid): bool { diff --git a/src/Module/Templates/DemoApi/src/Services/PostService.php.tpl b/src/Module/Templates/DemoApi/src/Services/PostService.php.tpl index b9a67c0f..6ac20b24 100644 --- a/src/Module/Templates/DemoApi/src/Services/PostService.php.tpl +++ b/src/Module/Templates/DemoApi/src/Services/PostService.php.tpl @@ -100,11 +100,11 @@ class PostService extends QtService /** * Get post * @param string $uuid - * @return Post + * @return Post|null * @throws BaseException * @throws ModelException */ - public function getPost(string $uuid): Post + public function getPost(string $uuid): ?Post { return $this->model ->joinTo(model(User::class)) @@ -158,7 +158,6 @@ class PostService extends QtService public function addPost(array $data): Post { $data['uuid'] = $data['uuid'] ?? uuid_ordered(); - $data['created_at'] = date('Y-m-d H:i:s'); $post = $this->model->create(); $post->fill($data); @@ -177,8 +176,6 @@ class PostService extends QtService */ public function updatePost(string $uuid, array $data): Post { - $data['updated_at'] = date('Y-m-d H:i:s'); - $post = $this->model->findOneBy('uuid', $uuid); $post->fill($data); $post->save(); diff --git a/src/Module/Templates/DemoWeb/src/Controllers/AccountController.php.tpl b/src/Module/Templates/DemoWeb/src/Controllers/AccountController.php.tpl index d81f2cb0..76e215c7 100644 --- a/src/Module/Templates/DemoWeb/src/Controllers/AccountController.php.tpl +++ b/src/Module/Templates/DemoWeb/src/Controllers/AccountController.php.tpl @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.9 + * @since 3.0.0 */ namespace {{MODULE_NAMESPACE}}\Controllers; @@ -34,7 +34,7 @@ class AccountController extends BaseController * Account service * @var AuthService */ - public $authService; + public AuthService $authService; /** * Works before an action diff --git a/src/Module/Templates/DemoWeb/src/Controllers/AuthController.php.tpl b/src/Module/Templates/DemoWeb/src/Controllers/AuthController.php.tpl index aaa7546e..1e064494 100644 --- a/src/Module/Templates/DemoWeb/src/Controllers/AuthController.php.tpl +++ b/src/Module/Templates/DemoWeb/src/Controllers/AuthController.php.tpl @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.9 + * @since 3.0.0 */ namespace {{MODULE_NAMESPACE}}\Controllers; @@ -29,32 +29,32 @@ class AuthController extends BaseController /** * Main layout */ - const LAYOUT = 'layouts/main'; + protected const LAYOUT = 'layouts/main'; /** * Signin view page */ - const VIEW_SIGNIN = 'auth/signin'; + protected const VIEW_SIGNIN = 'auth/signin'; /** * Signup view page */ - const VIEW_SIGNUP = 'auth/signup'; + protected const VIEW_SIGNUP = 'auth/signup'; /** * Forget view page */ - const VIEW_FORGET = 'auth/forget'; + protected const VIEW_FORGET = 'auth/forget'; /** * Reset view page */ - const VIEW_RESET = 'auth/reset'; + protected const VIEW_RESET = 'auth/reset'; /** * Verify view page */ - const VIEW_VERIFY = 'auth/verify'; + protected const VIEW_VERIFY = 'auth/verify'; /** * Action - sign in @@ -104,7 +104,7 @@ class AuthController extends BaseController if ($request->isMethod('post')) { $userData = $request->all(); - $userData['uuid'] = uuid_ordered(); + $userData['uuid'] = uuid_ordered(); $userData['role'] = Role::EDITOR; auth()->signup($userData); diff --git a/src/Module/Templates/DemoWeb/src/Controllers/BaseController.php.tpl b/src/Module/Templates/DemoWeb/src/Controllers/BaseController.php.tpl index 468c4198..25a9a8ee 100644 --- a/src/Module/Templates/DemoWeb/src/Controllers/BaseController.php.tpl +++ b/src/Module/Templates/DemoWeb/src/Controllers/BaseController.php.tpl @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.9 + * @since 3.0.0 */ namespace {{MODULE_NAMESPACE}}\Controllers; @@ -30,7 +30,7 @@ abstract class BaseController extends RouteController /** * @var QtView */ - protected $view; + protected QtView $view; /** * Works before an action diff --git a/src/Module/Templates/DemoWeb/src/Controllers/CommentController.php.tpl b/src/Module/Templates/DemoWeb/src/Controllers/CommentController.php.tpl index 6246aff2..3522dc49 100644 --- a/src/Module/Templates/DemoWeb/src/Controllers/CommentController.php.tpl +++ b/src/Module/Templates/DemoWeb/src/Controllers/CommentController.php.tpl @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.9 + * @since 3.0.0 */ namespace {{MODULE_NAMESPACE}}\Controllers; @@ -27,7 +27,7 @@ class CommentController extends BaseController /** * @var CommentService */ - public $commentService; + public CommentService $commentService; public function __before() { diff --git a/src/Module/Templates/DemoWeb/src/Controllers/PageController.php.tpl b/src/Module/Templates/DemoWeb/src/Controllers/PageController.php.tpl index 378601c5..7e4602a8 100644 --- a/src/Module/Templates/DemoWeb/src/Controllers/PageController.php.tpl +++ b/src/Module/Templates/DemoWeb/src/Controllers/PageController.php.tpl @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.9 + * @since 3.0.0 */ namespace {{MODULE_NAMESPACE}}\Controllers; @@ -27,7 +27,7 @@ class PageController extends BaseController /** * Main layout */ - const LAYOUT = 'layouts/main'; + protected const LAYOUT = 'layouts/main'; /** * Action - display home page diff --git a/src/Module/Templates/DemoWeb/src/Controllers/PostController.php.tpl b/src/Module/Templates/DemoWeb/src/Controllers/PostController.php.tpl index 646c27ab..6ca38e36 100644 --- a/src/Module/Templates/DemoWeb/src/Controllers/PostController.php.tpl +++ b/src/Module/Templates/DemoWeb/src/Controllers/PostController.php.tpl @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.9 + * @since 3.0.0 */ namespace {{MODULE_NAMESPACE}}\Controllers; @@ -31,23 +31,23 @@ class PostController extends BaseController /** * Posts per page */ - const POSTS_PER_PAGE = 8; + protected const POSTS_PER_PAGE = 8; /** * Current page */ - const CURRENT_PAGE = 1; + protected const CURRENT_PAGE = 1; /** * Main layout */ - const LAYOUT = 'layouts/main'; + protected const LAYOUT = 'layouts/main'; /** * Post service * @var PostService */ - public $postService; + public PostService $postService; public function __before() { diff --git a/src/Module/Templates/DemoWeb/src/Controllers/PostManagementController.php.tpl b/src/Module/Templates/DemoWeb/src/Controllers/PostManagementController.php.tpl index d31ed6b8..853fe263 100644 --- a/src/Module/Templates/DemoWeb/src/Controllers/PostManagementController.php.tpl +++ b/src/Module/Templates/DemoWeb/src/Controllers/PostManagementController.php.tpl @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.9 + * @since 3.0.0 */ namespace {{MODULE_NAMESPACE}}\Controllers; @@ -28,13 +28,13 @@ class PostManagementController extends BaseController /** * Main layout */ - const LAYOUT = 'layouts/main'; + protected const LAYOUT = 'layouts/main'; /** * Post service * @var PostService */ - public $postService; + public PostService $postService; public function __before() { @@ -195,7 +195,6 @@ class PostManagementController extends BaseController 'title' => $post->title, 'content' => $post->content, 'image' => '', - 'updated_at' => date('Y-m-d H:i:s'), ]); redirect(base_url(true) . '/' . current_lang() . '/my-posts'); diff --git a/src/Module/Templates/DemoWeb/src/Models/Comment.php.tpl b/src/Module/Templates/DemoWeb/src/Models/Comment.php.tpl index 746ff366..6a4911e4 100644 --- a/src/Module/Templates/DemoWeb/src/Models/Comment.php.tpl +++ b/src/Module/Templates/DemoWeb/src/Models/Comment.php.tpl @@ -15,6 +15,7 @@ namespace {{MODULE_NAMESPACE}}\Models; use Quantum\Libraries\Database\Enums\Relation; +use Quantum\Model\Traits\HasTimestamps; use Quantum\Model\Traits\SoftDeletes; use Quantum\Model\DbModel; @@ -25,6 +26,7 @@ use Quantum\Model\DbModel; class Comment extends DbModel { + use HasTimestamps; use SoftDeletes; /** @@ -48,8 +50,6 @@ class Comment extends DbModel 'post_uuid', 'user_uuid', 'content', - 'created_at', - 'updated_at', ]; /** diff --git a/src/Module/Templates/DemoWeb/src/Models/Post.php.tpl b/src/Module/Templates/DemoWeb/src/Models/Post.php.tpl index 7754aa7a..9f7ed5f0 100644 --- a/src/Module/Templates/DemoWeb/src/Models/Post.php.tpl +++ b/src/Module/Templates/DemoWeb/src/Models/Post.php.tpl @@ -15,6 +15,7 @@ namespace {{MODULE_NAMESPACE}}\Models; use Quantum\Libraries\Database\Enums\Relation; +use Quantum\Model\Traits\HasTimestamps; use Quantum\Model\Traits\SoftDeletes; use Quantum\Model\DbModel; @@ -25,6 +26,7 @@ use Quantum\Model\DbModel; class Post extends DbModel { + use HasTimestamps; use SoftDeletes; /** @@ -49,8 +51,6 @@ class Post extends DbModel 'title', 'content', 'image', - 'created_at', - 'updated_at' ]; /** diff --git a/src/Module/Templates/DemoWeb/src/Models/User.php.tpl b/src/Module/Templates/DemoWeb/src/Models/User.php.tpl index b4331053..d80e57d6 100644 --- a/src/Module/Templates/DemoWeb/src/Models/User.php.tpl +++ b/src/Module/Templates/DemoWeb/src/Models/User.php.tpl @@ -14,6 +14,7 @@ namespace {{MODULE_NAMESPACE}}\Models; +use Quantum\Model\Traits\HasTimestamps; use Quantum\Model\DbModel; /** @@ -23,6 +24,8 @@ use Quantum\Model\DbModel; class User extends DbModel { + use HasTimestamps; + /** * ID column of table * @var string @@ -55,7 +58,5 @@ class User extends DbModel 'otp', 'otp_expires', 'otp_token', - 'created_at', - 'updated_at', ]; } \ No newline at end of file diff --git a/src/Module/Templates/DemoWeb/src/Services/AuthService.php.tpl b/src/Module/Templates/DemoWeb/src/Services/AuthService.php.tpl index 4af8dd04..5a0d8556 100644 --- a/src/Module/Templates/DemoWeb/src/Services/AuthService.php.tpl +++ b/src/Module/Templates/DemoWeb/src/Services/AuthService.php.tpl @@ -57,12 +57,12 @@ class AuthService extends QtService implements AuthServiceInterface /** * Get user * @param string $uuid - * @return User + * @return User|null * @throws BaseException */ - public function getUserByUuid(string $uuid): User + public function getUserByUuid(string $uuid): ?User { - return$this->model->findOneBy('uuid', $uuid); + return $this->model->findOneBy('uuid', $uuid); } /** @@ -95,7 +95,6 @@ class AuthService extends QtService implements AuthServiceInterface public function add(array $data): AuthUser { $data['uuid'] = $data['uuid'] ?? uuid_ordered(); - $data['created_at'] = date('Y-m-d H:i:s'); $this->createUserDirectory($data['uuid']); @@ -123,8 +122,6 @@ class AuthService extends QtService implements AuthServiceInterface return null; } - $data['updated_at'] = date('Y-m-d H:i:s'); - $user->fill($data); $user->save(); diff --git a/src/Module/Templates/DemoWeb/src/Services/CommentService.php.tpl b/src/Module/Templates/DemoWeb/src/Services/CommentService.php.tpl index c93291d7..3528535c 100644 --- a/src/Module/Templates/DemoWeb/src/Services/CommentService.php.tpl +++ b/src/Module/Templates/DemoWeb/src/Services/CommentService.php.tpl @@ -78,6 +78,7 @@ class CommentService extends QtService * Get comment * @param string $uuid * @return Comment + * @throws BaseException */ public function getComment(string $uuid): Comment { @@ -93,7 +94,6 @@ class CommentService extends QtService public function addComment(array $data): array { $data['uuid'] = $data['uuid'] ?? uuid_ordered(); - $data['created_at'] = date('Y-m-d H:i:s'); $comment = $this->model->create(); $comment->fill($data); @@ -106,6 +106,8 @@ class CommentService extends QtService * Delete a comment * @param string $uuid * @return bool + * @throws BaseException + * @throws ModelException */ public function deleteComment(string $uuid): bool { diff --git a/src/Module/Templates/DemoWeb/src/Services/PostService.php.tpl b/src/Module/Templates/DemoWeb/src/Services/PostService.php.tpl index efb2a65c..7f484a65 100644 --- a/src/Module/Templates/DemoWeb/src/Services/PostService.php.tpl +++ b/src/Module/Templates/DemoWeb/src/Services/PostService.php.tpl @@ -156,7 +156,6 @@ class PostService extends QtService public function addPost(array $data): Post { $data['uuid'] = $data['uuid'] ?? uuid_ordered(); - $data['created_at'] = date('Y-m-d H:i:s'); $post = $this->model->create(); $post->fill($data); @@ -175,8 +174,6 @@ class PostService extends QtService */ public function updatePost(string $uuid, array $data): Post { - $data['updated_at'] = date('Y-m-d H:i:s'); - $post = $this->model->findOneBy('uuid', $uuid); $post->fill($data); $post->save(); diff --git a/src/Module/Templates/Toolkit/src/Controllers/BaseController.php.tpl b/src/Module/Templates/Toolkit/src/Controllers/BaseController.php.tpl index b8386b66..03a87853 100644 --- a/src/Module/Templates/Toolkit/src/Controllers/BaseController.php.tpl +++ b/src/Module/Templates/Toolkit/src/Controllers/BaseController.php.tpl @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.8 + * @since 3.0.0 */ namespace Modules\Toolkit\Controllers; @@ -25,26 +25,25 @@ use Quantum\View\QtView; */ class BaseController extends RouteController { - /** * Main layout */ - const LAYOUT = 'layouts/main'; + protected const LAYOUT = 'layouts/main'; /** * Items per page */ - const ITEMS_PER_PAGE = 20; + protected const ITEMS_PER_PAGE = 20; /** * Current page */ - const CURRENT_PAGE = 1; + protected const CURRENT_PAGE = 1; /** * @var QtView */ - protected $view; + protected QtView $view; /** * Works before an action @@ -61,4 +60,4 @@ class BaseController extends RouteController new Asset(Asset::JS, 'Toolkit/js/toolkit.js') ]); } -} \ No newline at end of file +} diff --git a/src/Module/Templates/Toolkit/src/Controllers/DashboardController.php.tpl b/src/Module/Templates/Toolkit/src/Controllers/DashboardController.php.tpl index 94276df2..0c9acba2 100644 --- a/src/Module/Templates/Toolkit/src/Controllers/DashboardController.php.tpl +++ b/src/Module/Templates/Toolkit/src/Controllers/DashboardController.php.tpl @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.9 + * @since 3.0.0 */ namespace Modules\Toolkit\Controllers; @@ -28,18 +28,13 @@ use ReflectionException; */ class DashboardController extends BaseController { - /** - * Email service * @var DashboardService */ - public $dashboardService; + public DashboardService $dashboardService; /** - * @throws DiException - * @throws ReflectionException - * @throws ServiceException - * @throws BaseException + * Works before an action */ public function __before() { @@ -60,4 +55,4 @@ class DashboardController extends BaseController $response->html($this->view->render('pages/dashboard/index')); } -} \ No newline at end of file +} diff --git a/src/Module/Templates/Toolkit/src/Controllers/DatabaseController.php.tpl b/src/Module/Templates/Toolkit/src/Controllers/DatabaseController.php.tpl index c14c48f8..6a1923fe 100644 --- a/src/Module/Templates/Toolkit/src/Controllers/DatabaseController.php.tpl +++ b/src/Module/Templates/Toolkit/src/Controllers/DatabaseController.php.tpl @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.9 + * @since 3.0.0 */ namespace Modules\Toolkit\Controllers; @@ -25,11 +25,10 @@ use Quantum\Http\Request; */ class DatabaseController extends BaseController { - /** * @var DatabaseService */ - private $databaseService; + private DatabaseService $databaseService; /** * Works before an action @@ -122,4 +121,4 @@ class DatabaseController extends BaseController redirect(base_url(true) . '/database/view?table=' . $tableName); } -} \ No newline at end of file +} diff --git a/src/Module/Templates/Toolkit/src/Controllers/EmailsController.php.tpl b/src/Module/Templates/Toolkit/src/Controllers/EmailsController.php.tpl index ce989073..08867a26 100644 --- a/src/Module/Templates/Toolkit/src/Controllers/EmailsController.php.tpl +++ b/src/Module/Templates/Toolkit/src/Controllers/EmailsController.php.tpl @@ -9,11 +9,12 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.9 + * @since 3.0.0 */ namespace Modules\Toolkit\Controllers; +use Quantum\App\Exceptions\BaseException; use Quantum\Service\Exceptions\ServiceException; use Modules\Toolkit\Services\EmailService; use Quantum\Di\Exceptions\DiException; @@ -27,17 +28,13 @@ use ReflectionException; */ class EmailsController extends BaseController { - /** - * Email service * @var EmailService */ - public $emailService; + public EmailService $emailService; /** - * @throws DiException - * @throws ServiceException - * @throws ReflectionException + * Works before an action */ public function __before() { @@ -86,4 +83,4 @@ class EmailsController extends BaseController redirect(base_url(true) . '/emails'); } -} \ No newline at end of file +} diff --git a/src/Module/Templates/Toolkit/src/Controllers/LogsController.php.tpl b/src/Module/Templates/Toolkit/src/Controllers/LogsController.php.tpl index 14880291..44d00ac1 100644 --- a/src/Module/Templates/Toolkit/src/Controllers/LogsController.php.tpl +++ b/src/Module/Templates/Toolkit/src/Controllers/LogsController.php.tpl @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.9 + * @since 3.0.0 */ namespace Modules\Toolkit\Controllers; @@ -28,18 +28,13 @@ use ReflectionException; */ class LogsController extends BaseController { - /** - * Logs service * @var LogsService */ - public $logsService; + public LogsService $logsService; /** - * @throws DiException - * @throws ReflectionException - * @throws ServiceException - * @throws BaseException + * Works before an action */ public function __before() { @@ -51,7 +46,8 @@ class LogsController extends BaseController /** * @param Response $response */ - public function list(Response $response){ + public function list(Response $response) + { $filteredLogFiles = $this->logsService->getLogFiles(); $this->view->setParams([ @@ -66,7 +62,8 @@ class LogsController extends BaseController * @param Request $request * @param Response $response */ - public function single(Request $request, Response $response){ + public function single(Request $request, Response $response) + { $logFile = $request->get('logFile'); $perPage = $request->get('per_page', self::ITEMS_PER_PAGE); $currentPage = $request->get('page', self::CURRENT_PAGE); @@ -83,4 +80,4 @@ class LogsController extends BaseController $response->html($this->view->render('pages/logs/log')); } -} \ No newline at end of file +} diff --git a/src/Module/Templates/Toolkit/src/Middlewares/BaseMiddleware.php.tpl b/src/Module/Templates/Toolkit/src/Middlewares/BaseMiddleware.php.tpl index 261e0b0d..8ad5bf48 100644 --- a/src/Module/Templates/Toolkit/src/Middlewares/BaseMiddleware.php.tpl +++ b/src/Module/Templates/Toolkit/src/Middlewares/BaseMiddleware.php.tpl @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.9 + * @since 3.0.0 */ namespace Modules\Toolkit\Middlewares; @@ -25,11 +25,10 @@ use Quantum\Http\Request; */ abstract class BaseMiddleware extends QtMiddleware { - /** * @var Validator */ - protected $validator; + protected Validator $validator; /** * Initialize Validator and define rules. @@ -72,4 +71,4 @@ abstract class BaseMiddleware extends QtMiddleware { // default no-op: subclasses override if needed } -} \ No newline at end of file +} diff --git a/src/Module/Templates/Toolkit/src/Middlewares/BasicAuth.php.tpl b/src/Module/Templates/Toolkit/src/Middlewares/BasicAuth.php.tpl index b9f62008..35025b52 100644 --- a/src/Module/Templates/Toolkit/src/Middlewares/BasicAuth.php.tpl +++ b/src/Module/Templates/Toolkit/src/Middlewares/BasicAuth.php.tpl @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.8 + * @since 3.0.0 */ namespace Modules\Toolkit\Middlewares; @@ -26,7 +26,6 @@ use Closure; */ class BasicAuth extends QtMiddleware { - /** * @param Request $request * @param Response $response @@ -69,4 +68,4 @@ class BasicAuth extends QtMiddleware $response->html(partial('errors' . DS . '401'), 401); stop(); } -} \ No newline at end of file +} diff --git a/src/Module/Templates/Toolkit/src/Middlewares/CreateTable.php.tpl b/src/Module/Templates/Toolkit/src/Middlewares/CreateTable.php.tpl index 0a350551..0478e88c 100644 --- a/src/Module/Templates/Toolkit/src/Middlewares/CreateTable.php.tpl +++ b/src/Module/Templates/Toolkit/src/Middlewares/CreateTable.php.tpl @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.9 + * @since 3.0.0 */ namespace Modules\Toolkit\Middlewares; @@ -27,7 +27,6 @@ use Closure; */ class CreateTable extends BaseMiddleware { - /** * @param Request $request * @param Response $response @@ -87,4 +86,4 @@ class CreateTable extends BaseMiddleware return is_array($decoded) && !empty($decoded); }); } -} \ No newline at end of file +} diff --git a/src/Module/Templates/Toolkit/src/Services/DashboardService.php.tpl b/src/Module/Templates/Toolkit/src/Services/DashboardService.php.tpl index 0d63513b..431ee978 100644 --- a/src/Module/Templates/Toolkit/src/Services/DashboardService.php.tpl +++ b/src/Module/Templates/Toolkit/src/Services/DashboardService.php.tpl @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.8 + * @since 3.0.0 */ namespace Modules\Toolkit\Services; @@ -22,8 +22,7 @@ use Quantum\Service\QtService; */ class DashboardService extends QtService { - public function __construct() { } -} \ No newline at end of file +} diff --git a/src/Module/Templates/Toolkit/src/Services/DatabaseService.php.tpl b/src/Module/Templates/Toolkit/src/Services/DatabaseService.php.tpl index e848e733..d8c01334 100644 --- a/src/Module/Templates/Toolkit/src/Services/DatabaseService.php.tpl +++ b/src/Module/Templates/Toolkit/src/Services/DatabaseService.php.tpl @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.9 + * @since 3.0.0 */ namespace Modules\Toolkit\Services; @@ -28,11 +28,10 @@ use ReflectionException; */ class DatabaseService extends QtService { - /** * @var string */ - protected $storeDirectory; + protected string $storeDirectory; public function __construct() { @@ -180,4 +179,4 @@ class DatabaseService extends QtService return $columns; } -} \ No newline at end of file +} diff --git a/src/Module/Templates/Toolkit/src/Services/EmailService.php.tpl b/src/Module/Templates/Toolkit/src/Services/EmailService.php.tpl index 4182f70a..4280317f 100644 --- a/src/Module/Templates/Toolkit/src/Services/EmailService.php.tpl +++ b/src/Module/Templates/Toolkit/src/Services/EmailService.php.tpl @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.8 + * @since 3.0.0 */ namespace Modules\Toolkit\Services; @@ -29,11 +29,10 @@ use ReflectionException; */ class EmailService extends QtService { - /** * @var string */ - private $emailsDirectory; + private string $emailsDirectory; public function __construct() @@ -130,4 +129,4 @@ class EmailService extends QtService "page" => $currentPage ]); } -} \ No newline at end of file +} diff --git a/src/Module/Templates/Toolkit/src/Services/LogsService.php.tpl b/src/Module/Templates/Toolkit/src/Services/LogsService.php.tpl index 82f6c2e1..c3f9130f 100644 --- a/src/Module/Templates/Toolkit/src/Services/LogsService.php.tpl +++ b/src/Module/Templates/Toolkit/src/Services/LogsService.php.tpl @@ -9,7 +9,7 @@ * @author Arman Ag. * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org) * @link http://quantum.softberg.org/ - * @since 2.9.8 + * @since 3.0.0 */ namespace Modules\Toolkit\Services; @@ -29,7 +29,6 @@ use ReflectionException; */ class LogsService extends QtService { - /** * Retrieves a list of available log file names from the logs directory. * @return array @@ -104,4 +103,4 @@ class LogsService extends QtService "page" => $currentPage ]); } -} \ No newline at end of file +} diff --git a/src/Service/Helpers/service.php b/src/Service/Helpers/service.php index ff58b677..62f0fe72 100644 --- a/src/Service/Helpers/service.php +++ b/src/Service/Helpers/service.php @@ -20,14 +20,16 @@ /** * Gets or creates service instance - * @param string $serviceClass + * @param class-string $serviceClass * @param bool $singleton - * @return QtService - * @throws ReflectionException + * @return T * @throws BaseException * @throws DiException + * @throws ReflectionException * @throws ServiceException + * @template T of QtService */ + function service(string $serviceClass, bool $singleton = false): QtService { return $singleton ? ServiceFactory::get($serviceClass) : ServiceFactory::create($serviceClass); diff --git a/tests/Unit/Model/DbModelTimestampsTest.php b/tests/Unit/Model/DbModelTimestampsTest.php new file mode 100644 index 00000000..92a92967 --- /dev/null +++ b/tests/Unit/Model/DbModelTimestampsTest.php @@ -0,0 +1,184 @@ +set('app.debug', true); + + IdiormDbal::connect(['driver' => 'sqlite', 'database' => ':memory:']); + + $this->createPostsTable(); + } + + public function tearDown(): void + { + IdiormDbal::execute('DROP TABLE posts'); + IdiormDbal::execute('DROP TABLE posts_custom'); + } + + public function testTimestampsAreNotAppliedWhenTraitIsNotUsed() + { + /** @var TestPostModel $model */ + $model = ModelFactory::get(TestPostModel::class); + + $post = $model->create(); + + $post->title = 'Hello'; + $post->content = 'World'; + $post->author = 'John'; + $post->published_at = '2026-01-23 10:00:00'; + + $this->assertTrue($post->save()); + + $saved = $model->findOne($post->id); + + $this->assertNotNull($saved); + + $data = $saved->asArray(); + + $this->assertArrayHasKey('created_at', $data); + $this->assertNull($data['created_at']); + + $this->assertArrayHasKey('updated_at', $data); + $this->assertNull($data['updated_at']); + } + + public function testTimestampsAreAppliedOnInsertWhenTraitIsUsed() + { + /** @var TestPostTimestampModel $model */ + $model = ModelFactory::get(TestPostTimestampModel::class); + + $post = $model->create(); + + $post->title = 'Hello'; + $post->content = 'World'; + $post->author = 'John'; + $post->published_at = '2026-01-23 10:00:00'; + + $this->assertTrue($post->save()); + + $saved = $model->findOne($post->id); + + $this->assertNotNull($saved); + + $this->assertNotEmpty($saved->created_at); + $this->assertNotEmpty($saved->updated_at); + } + + public function testUpdatedAtChangesOnUpdateButCreatedAtStaysSame() + { + /** @var TestPostTimestampModel $model */ + $model = ModelFactory::get(TestPostTimestampModel::class); + + $post = $model->create(); + + $post->title = 'First'; + $post->content = 'Body'; + $post->author = 'John'; + $post->published_at = '2026-01-23 10:00:00'; + + $this->assertTrue($post->save()); + + $saved = $model->findOne($post->id); + + $this->assertNotNull($saved); + + $createdAt1 = $saved->created_at; + $updatedAt1 = $saved->updated_at; + + sleep(1); + + $saved->title = 'Second'; + + $this->assertTrue($saved->save()); + + $saved2 = $model->findOne($post->id); + + $this->assertNotNull($saved2); + + $this->assertEquals($createdAt1, $saved2->created_at); + $this->assertNotEquals($updatedAt1, $saved2->updated_at); + } + + public function testUnixTimestampTypeStoresIntegers() + { + /** @var TestPostUnixTimestampModel $model */ + $model = ModelFactory::get(TestPostUnixTimestampModel::class); + + $post = $model->create(); + + $post->title = 'Unix'; + $post->content = 'Test'; + $post->author = 'John'; + $post->published_at = '2026-01-23 10:00:00'; + + $this->assertTrue($post->save()); + + $saved = $model->findOne($post->id); + + $this->assertNotNull($saved); + + $this->assertIsNumeric($saved->created_at); + $this->assertIsNumeric($saved->updated_at); + } + + public function testCustomTimestampColumnsAreApplied() + { + /** @var TestPostCustomTimestampModel $model */ + $model = ModelFactory::get(TestPostCustomTimestampModel::class); + + $post = $model->create(); + + $post->title = 'Custom'; + $post->content = 'Columns'; + + $this->assertTrue($post->save()); + + $saved = $model->findOne($post->id); + + $this->assertNotNull($saved); + + $data = $saved->asArray(); + + $this->assertArrayHasKey('created_on', $data); + $this->assertArrayHasKey('modified_on', $data); + + $this->assertNotEmpty($data['created_on']); + $this->assertNotEmpty($data['modified_on']); + } + + private function createPostsTable(): void + { + IdiormDbal::execute('CREATE TABLE IF NOT EXISTS posts ( + id INTEGER PRIMARY KEY, + title VARCHAR(255), + content TEXT, + author VARCHAR(255), + published_at TEXT NULL, + created_at TEXT NULL, + updated_at TEXT NULL + )'); + + IdiormDbal::execute('CREATE TABLE IF NOT EXISTS posts_custom ( + id INTEGER PRIMARY KEY, + title VARCHAR(255), + content TEXT, + created_on TEXT NULL, + modified_on TEXT NULL + )'); + } + +} diff --git a/tests/_root/shared/Models/TestPostCustomTimestampModel.php b/tests/_root/shared/Models/TestPostCustomTimestampModel.php new file mode 100644 index 00000000..27be6d75 --- /dev/null +++ b/tests/_root/shared/Models/TestPostCustomTimestampModel.php @@ -0,0 +1,24 @@ + Date: Sun, 25 Jan 2026 16:18:28 +0400 Subject: [PATCH 2/4] cs:fix --- .../Console/Commands/CronRunCommandTest.php | 24 ++++++++++++------- tests/Unit/Hook/HookManagerTest.php | 6 +++-- tests/Unit/Libraries/Cron/CronHelperTest.php | 3 ++- tests/Unit/Libraries/Cron/CronTaskTest.php | 24 ++++++++++++------- tests/Unit/Libraries/Cron/ScheduleTest.php | 6 +++-- .../Unit/Router/Helpers/RouterHelperTest.php | 3 ++- 6 files changed, 44 insertions(+), 22 deletions(-) diff --git a/tests/Unit/Console/Commands/CronRunCommandTest.php b/tests/Unit/Console/Commands/CronRunCommandTest.php index 965bc661..c9641b72 100644 --- a/tests/Unit/Console/Commands/CronRunCommandTest.php +++ b/tests/Unit/Console/Commands/CronRunCommandTest.php @@ -55,7 +55,8 @@ public function testCommandExecutesSuccessfully() $this->createTaskFile('test-task.php', [ 'name' => 'test-task', 'expression' => '* * * * *', - 'callback' => function () {}, + 'callback' => function () { + }, ]); $command = new CronRunCommand(); @@ -90,7 +91,8 @@ public function testCommandWithSpecificTask() $this->createTaskFile('specific-task.php', [ 'name' => 'specific-task', 'expression' => '* * * * *', - 'callback' => function () {}, + 'callback' => function () { + }, ]); $command = new CronRunCommand(); @@ -111,7 +113,8 @@ public function testCommandWithForceOption() $this->createTaskFile('force-task.php', [ 'name' => 'force-task', 'expression' => '* * * * *', - 'callback' => function () {}, + 'callback' => function () { + }, ]); $command = new CronRunCommand(); @@ -145,13 +148,15 @@ public function testCommandDisplaysStatistics() $this->createTaskFile('task1.php', [ 'name' => 'task-1', 'expression' => '* * * * *', - 'callback' => function () {}, + 'callback' => function () { + }, ]); $this->createTaskFile('task2.php', [ 'name' => 'task-2', 'expression' => '0 0 1 1 *', - 'callback' => function () {}, + 'callback' => function () { + }, ]); $command = new CronRunCommand(); @@ -189,7 +194,8 @@ public function testCommandShortOptions() $this->createTaskFile('short-option-task.php', [ 'name' => 'short-option-task', 'expression' => '* * * * *', - 'callback' => function () {}, + 'callback' => function () { + }, ]); $command = new CronRunCommand(); @@ -210,7 +216,8 @@ public function testCommandUsesConfiguredPath() $this->createTaskFile('config-task.php', [ 'name' => 'config-task', 'expression' => '* * * * *', - 'callback' => function () {}, + 'callback' => function () { + }, ]); config()->set('cron', [ @@ -234,7 +241,8 @@ public function testCommandReportsLockedTasks() $this->createTaskFile('locked-task.php', [ 'name' => 'locked-task', 'expression' => '* * * * *', - 'callback' => function () {}, + 'callback' => function () { + }, ]); $lock = new \Quantum\Libraries\Cron\CronLock('locked-task', $this->lockDirectory); diff --git a/tests/Unit/Hook/HookManagerTest.php b/tests/Unit/Hook/HookManagerTest.php index ffb1d5b7..52817bcc 100644 --- a/tests/Unit/Hook/HookManagerTest.php +++ b/tests/Unit/Hook/HookManagerTest.php @@ -81,7 +81,8 @@ public function testUnregisteredHookThrowsOnOn() $this->expectException(HookException::class); $this->expectExceptionMessage('The Hook `INVALID` was not registered.'); - hook()->on('INVALID', function () {}); + hook()->on('INVALID', function () { + }); } public function testUnregisteredHookThrowsOnFire() @@ -94,7 +95,8 @@ public function testUnregisteredHookThrowsOnFire() public function testGetRegisteredReturnsHookStore() { - hook()->on('SAVE', function () {}); + hook()->on('SAVE', function () { + }); $store = HookManager::getRegistered(); diff --git a/tests/Unit/Libraries/Cron/CronHelperTest.php b/tests/Unit/Libraries/Cron/CronHelperTest.php index 0d2865f2..2deacbb1 100644 --- a/tests/Unit/Libraries/Cron/CronHelperTest.php +++ b/tests/Unit/Libraries/Cron/CronHelperTest.php @@ -35,7 +35,8 @@ public function testCronManagerHelper() public function testCronTaskHelper() { - $task = cron_task('my-task', '* * * * *', function () {}); + $task = cron_task('my-task', '* * * * *', function () { + }); $this->assertInstanceOf(CronTask::class, $task); $this->assertEquals('my-task', $task->getName()); } diff --git a/tests/Unit/Libraries/Cron/CronTaskTest.php b/tests/Unit/Libraries/Cron/CronTaskTest.php index c03bb4ec..b0f4cc2c 100644 --- a/tests/Unit/Libraries/Cron/CronTaskTest.php +++ b/tests/Unit/Libraries/Cron/CronTaskTest.php @@ -14,7 +14,8 @@ class CronTaskTest extends AppTestCase { public function testConstructorWithValidExpression() { - $task = new CronTask('test-task', '* * * * *', function () {}); + $task = new CronTask('test-task', '* * * * *', function () { + }); $this->assertEquals('test-task', $task->getName()); $this->assertEquals('* * * * *', $task->getExpression()); @@ -25,12 +26,14 @@ public function testConstructorWithInvalidExpression() $this->expectException(CronException::class); $this->expectExceptionMessage('Invalid cron expression'); - new CronTask('test-task', 'invalid', function () {}); + new CronTask('test-task', 'invalid', function () { + }); } public function testShouldRunEveryMinute() { - $task = new CronTask('test-task', '* * * * *', function () {}); + $task = new CronTask('test-task', '* * * * *', function () { + }); $this->assertTrue($task->shouldRun()); } @@ -38,7 +41,8 @@ public function testShouldRunEveryMinute() public function testShouldNotRunFutureTask() { // Task scheduled for next year - $task = new CronTask('test-task', '0 0 1 1 *', function () {}); + $task = new CronTask('test-task', '0 0 1 1 *', function () { + }); $this->assertFalse($task->shouldRun()); } @@ -71,7 +75,8 @@ public function testHandleWithCallbackArguments() public function testGetNextRunDate() { - $task = new CronTask('test-task', '0 0 * * *', function () {}); + $task = new CronTask('test-task', '0 0 * * *', function () { + }); $nextRun = $task->getNextRunDate(); @@ -81,7 +86,8 @@ public function testGetNextRunDate() public function testGetPreviousRunDate() { - $task = new CronTask('test-task', '0 0 * * *', function () {}); + $task = new CronTask('test-task', '0 0 * * *', function () { + }); $previousRun = $task->getPreviousRunDate(); @@ -92,7 +98,8 @@ public function testGetPreviousRunDate() public function testComplexCronExpression() { // Every 5 minutes - $task = new CronTask('test-task', '*/5 * * * *', function () {}); + $task = new CronTask('test-task', '*/5 * * * *', function () { + }); $this->assertEquals('*/5 * * * *', $task->getExpression()); } @@ -100,7 +107,8 @@ public function testComplexCronExpression() public function testWeeklyCronExpression() { // Every Monday at 9 AM - $task = new CronTask('test-task', '0 9 * * 1', function () {}); + $task = new CronTask('test-task', '0 9 * * 1', function () { + }); $this->assertEquals('0 9 * * 1', $task->getExpression()); } diff --git a/tests/Unit/Libraries/Cron/ScheduleTest.php b/tests/Unit/Libraries/Cron/ScheduleTest.php index e2d92309..423b9a76 100644 --- a/tests/Unit/Libraries/Cron/ScheduleTest.php +++ b/tests/Unit/Libraries/Cron/ScheduleTest.php @@ -216,7 +216,8 @@ public function testCronSchedulesCustomExpression() public function testBuildSetsTask() { - $callback = function () {}; + $callback = function () { + }; $task = $this->schedule->everyMinute()->call($callback)->build(); $this->assertInstanceOf(CronTask::class, $task); @@ -233,7 +234,8 @@ public function testBuildThrowsExceptionWhenCallbackMissing() public function testBuildThrowsExceptionWhenScheduleMissing() { - $callback = function () {}; + $callback = function () { + }; $this->expectException(CronException::class); $this->expectExceptionMessage("Task 'test-task' must have a schedule. Use methods like daily(), hourly(), etc."); $this->schedule->call($callback)->build(); diff --git a/tests/Unit/Router/Helpers/RouterHelperTest.php b/tests/Unit/Router/Helpers/RouterHelperTest.php index d3b9da80..1d4269f0 100644 --- a/tests/Unit/Router/Helpers/RouterHelperTest.php +++ b/tests/Unit/Router/Helpers/RouterHelperTest.php @@ -94,7 +94,8 @@ public function testMvcRouteCallback() [ 'route' => 'home', 'method' => 'GET', - 'callback' => function (Response $response) {}, + 'callback' => function (Response $response) { + }, 'module' => 'Test', ], ]); From 573daba85c8314ca662b18d6ef886ddd3d47b276 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Mon, 26 Jan 2026 11:01:48 +0400 Subject: [PATCH 3/4] Fixing soft delete relate test cases --- ...tDeletesIdiOrm.php => ModelSoftDeletesIdiOrmTest.php} | 2 +- ...oftDeletesSleek.php => ModelSoftDeletesSleekTest.php} | 9 +++++++-- tests/Unit/Paginator/PaginatorTestCase.php | 3 +++ 3 files changed, 11 insertions(+), 3 deletions(-) rename tests/Unit/Model/{ModelSoftDeletesIdiOrm.php => ModelSoftDeletesIdiOrmTest.php} (99%) rename tests/Unit/Model/{ModelSoftDeletesSleek.php => ModelSoftDeletesSleekTest.php} (95%) diff --git a/tests/Unit/Model/ModelSoftDeletesIdiOrm.php b/tests/Unit/Model/ModelSoftDeletesIdiOrmTest.php similarity index 99% rename from tests/Unit/Model/ModelSoftDeletesIdiOrm.php rename to tests/Unit/Model/ModelSoftDeletesIdiOrmTest.php index 3c1a2908..d850088b 100644 --- a/tests/Unit/Model/ModelSoftDeletesIdiOrm.php +++ b/tests/Unit/Model/ModelSoftDeletesIdiOrmTest.php @@ -9,7 +9,7 @@ use Quantum\Model\ModelCollection; use Quantum\Paginator\Paginator; -class ModelSoftDeletesIdiOrm extends AppTestCase +class ModelSoftDeletesIdiOrmTest extends AppTestCase { private $model; diff --git a/tests/Unit/Model/ModelSoftDeletesSleek.php b/tests/Unit/Model/ModelSoftDeletesSleekTest.php similarity index 95% rename from tests/Unit/Model/ModelSoftDeletesSleek.php rename to tests/Unit/Model/ModelSoftDeletesSleekTest.php index bd0ec8ce..e55cd957 100644 --- a/tests/Unit/Model/ModelSoftDeletesSleek.php +++ b/tests/Unit/Model/ModelSoftDeletesSleekTest.php @@ -5,12 +5,13 @@ use Quantum\Libraries\Database\Adapters\Sleekdb\SleekDbal; use Quantum\Tests\_root\shared\Models\TestProductsModel; use Quantum\Model\Factories\ModelFactory; +use Quantum\Libraries\Database\Database; use Quantum\Tests\Unit\AppTestCase; use Quantum\Model\ModelCollection; use Quantum\Paginator\Paginator; use Quantum\Loader\Setup; -class ModelSoftDeletesSleek extends AppTestCase +class ModelSoftDeletesSleekTest extends AppTestCase { private $model; @@ -18,7 +19,11 @@ public function setUp(): void { parent::setUp(); - config()->import(new Setup('config', 'database')); + $this->setPrivateProperty(Database::class, 'instance', null); + + if (!config()->has('database')) { + config()->import(new Setup('config', 'database')); + } config()->set('database.default', 'sleekdb'); diff --git a/tests/Unit/Paginator/PaginatorTestCase.php b/tests/Unit/Paginator/PaginatorTestCase.php index cc0e4872..aed74608 100644 --- a/tests/Unit/Paginator/PaginatorTestCase.php +++ b/tests/Unit/Paginator/PaginatorTestCase.php @@ -3,6 +3,7 @@ namespace Quantum\Tests\Unit\Paginator; use Quantum\Libraries\Database\Adapters\Idiorm\IdiormDbal; +use Quantum\Libraries\Database\Database; use Quantum\Tests\Unit\AppTestCase; class PaginatorTestCase extends AppTestCase @@ -11,6 +12,8 @@ public function setUp(): void { parent::setUp(); + $this->setPrivateProperty(Database::class, 'instance', null); + IdiormDbal::connect(['driver' => 'sqlite', 'database' => ':memory:']); $this->_createPostTableWithData(); From e60f11c4b070836143f6abcd8ff3b93ad0a5996d Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Mon, 26 Jan 2026 19:47:42 +0400 Subject: [PATCH 4/4] Minor enhancements requested at review --- src/Model/Traits/HasTimestamps.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Model/Traits/HasTimestamps.php b/src/Model/Traits/HasTimestamps.php index 9c82cf3a..cde40ad8 100644 --- a/src/Model/Traits/HasTimestamps.php +++ b/src/Model/Traits/HasTimestamps.php @@ -48,7 +48,7 @@ protected function isNewRecord(): bool { $id = $this->attributes[$this->idColumn] ?? null; - return empty($id); + return $id === null || $id === ''; } /** @@ -88,7 +88,7 @@ protected function getCreatedAtColumn(): string return static::CREATED_AT; } - return 'created_at'; + return $this->createdAt; } /** @@ -101,7 +101,7 @@ protected function getUpdatedAtColumn(): string return static::UPDATED_AT; } - return 'updated_at'; + return $this->updatedAt; } /**