diff --git a/.github/docker/ci/smoke.php b/.github/docker/ci/smoke.php index a65b353208..a43a064dc5 100644 --- a/.github/docker/ci/smoke.php +++ b/.github/docker/ci/smoke.php @@ -257,6 +257,8 @@ function setting(string $name) check('one document seeded', count_rows('site_content') === 1, 'rows: ' . count_rows('site_content')); check('the document is the install success page', ($home['alias'] ?? '') === 'minimal-base', 'alias: ' . ($home['alias'] ?? 'none')); check('the document uses the seeded template', (int) ($home['template'] ?? 0) === 1); + $templateSource = $pdo->query('SELECT templatesource FROM ' . t('site_templates') . ' WHERE id = 1')->fetchColumn(); + check('the minimal template uses database rendering', $templateSource === 'db', 'source: ' . ($templateSource ?: 'empty')); } check('the document is published', (int) ($home['published'] ?? 0) === 1); check('a template was seeded', count_rows('site_templates') >= 1); diff --git a/core/database/seeders/SiteTemplatesTableSeeder.php b/core/database/seeders/SiteTemplatesTableSeeder.php index a2848cc0ef..977e803bda 100644 --- a/core/database/seeders/SiteTemplatesTableSeeder.php +++ b/core/database/seeders/SiteTemplatesTableSeeder.php @@ -31,6 +31,7 @@ public function run(): void [ 'templatename' => 'Minimal Template', 'templatealias' => '', + 'templatesource' => 'db', 'description' => 'Default minimal empty template (content returned only)', 'editor_type' => 0, 'category' => 0, diff --git a/core/src/Models/ActiveUser.php b/core/src/Models/ActiveUser.php index 5a46a38cbb..b03560f94d 100644 --- a/core/src/Models/ActiveUser.php +++ b/core/src/Models/ActiveUser.php @@ -18,6 +18,8 @@ */ class ActiveUser extends Eloquent\Model { + protected $table = 'active_users'; + protected $primaryKey = 'sid'; public $incrementing = false; public $timestamps = false; diff --git a/core/src/Models/ActiveUserLock.php b/core/src/Models/ActiveUserLock.php index 38f784e7d7..2cfedc7bec 100644 --- a/core/src/Models/ActiveUserLock.php +++ b/core/src/Models/ActiveUserLock.php @@ -16,6 +16,8 @@ */ class ActiveUserLock extends Eloquent\Model { + protected $table = 'active_user_locks'; + public $timestamps = false; protected $casts = [ diff --git a/core/src/Models/ActiveUserSession.php b/core/src/Models/ActiveUserSession.php index f6acc6d850..e34c208aef 100644 --- a/core/src/Models/ActiveUserSession.php +++ b/core/src/Models/ActiveUserSession.php @@ -14,6 +14,8 @@ */ class ActiveUserSession extends Eloquent\Model { + protected $table = 'active_user_sessions'; + protected $primaryKey = 'sid'; public $incrementing = false; public $timestamps = false; diff --git a/core/src/Models/Category.php b/core/src/Models/Category.php index 17c7b28b6d..bc4c4549b3 100644 --- a/core/src/Models/Category.php +++ b/core/src/Models/Category.php @@ -27,6 +27,8 @@ class Category extends Eloquent\Model { use Traits\Models\ManagerActions; + protected $table = 'categories'; + public $timestamps = false; protected $casts = [ diff --git a/core/src/Models/DocumentGroup.php b/core/src/Models/DocumentGroup.php index 2955c75480..e8c5c72388 100644 --- a/core/src/Models/DocumentGroup.php +++ b/core/src/Models/DocumentGroup.php @@ -13,6 +13,8 @@ */ class DocumentGroup extends Eloquent\Model { + protected $table = 'document_groups'; + public $timestamps = false; protected $casts = [ diff --git a/core/src/Models/DocumentgroupName.php b/core/src/Models/DocumentgroupName.php index 08d98f928c..0597bfe3f4 100644 --- a/core/src/Models/DocumentgroupName.php +++ b/core/src/Models/DocumentgroupName.php @@ -16,6 +16,8 @@ */ class DocumentgroupName extends Eloquent\Model { + protected $table = 'documentgroup_names'; + public $timestamps = false; protected $casts = [ diff --git a/core/src/Models/FileGroup.php b/core/src/Models/FileGroup.php index 5a4a508dbd..91e9130b5a 100644 --- a/core/src/Models/FileGroup.php +++ b/core/src/Models/FileGroup.php @@ -13,6 +13,8 @@ */ class FileGroup extends Eloquent\Model { + protected $table = 'file_groups'; + public $timestamps = false; protected $casts = [ diff --git a/core/src/Models/MemberGroup.php b/core/src/Models/MemberGroup.php index 278b37a667..2e4cbeb9ea 100644 --- a/core/src/Models/MemberGroup.php +++ b/core/src/Models/MemberGroup.php @@ -16,6 +16,8 @@ class MemberGroup extends Eloquent\Model { use Traits\Models\ManagerActions; + protected $table = 'member_groups'; + public $timestamps = false; protected $casts = [ diff --git a/core/src/Models/MembergroupName.php b/core/src/Models/MembergroupName.php index 651ef7477e..e40e93587b 100644 --- a/core/src/Models/MembergroupName.php +++ b/core/src/Models/MembergroupName.php @@ -16,6 +16,8 @@ */ class MembergroupName extends Eloquent\Model { + protected $table = 'membergroup_names'; + public $timestamps = false; protected $fillable = [ diff --git a/core/src/Models/Permissions.php b/core/src/Models/Permissions.php index 00a57fc2d4..fd558e0992 100644 --- a/core/src/Models/Permissions.php +++ b/core/src/Models/Permissions.php @@ -24,6 +24,8 @@ class Permissions extends Eloquent\Model { use ManagerActions; + protected $table = 'permissions'; + protected $managerActionsMap = [ 'actions.cancel' => 86, 'actions.new' => 135, diff --git a/core/src/Models/PermissionsGroups.php b/core/src/Models/PermissionsGroups.php index 7e56337aa4..f4acc22a0c 100644 --- a/core/src/Models/PermissionsGroups.php +++ b/core/src/Models/PermissionsGroups.php @@ -22,6 +22,8 @@ class PermissionsGroups extends Eloquent\Model { use ManagerActions; + protected $table = 'permissions_groups'; + protected $managerActionsMap = [ 'actions.cancel' => 86, 'actions.new' => 136, diff --git a/core/src/Models/RolePermissions.php b/core/src/Models/RolePermissions.php index 4a3eaf3562..b29ea46a21 100644 --- a/core/src/Models/RolePermissions.php +++ b/core/src/Models/RolePermissions.php @@ -19,6 +19,7 @@ */ class RolePermissions extends Eloquent\Model { + protected $table = 'role_permissions'; protected $fillable = [ 'permission', diff --git a/core/src/Models/SiteHtmlsnippet.php b/core/src/Models/SiteHtmlsnippet.php index 50f40917e5..11291ce327 100644 --- a/core/src/Models/SiteHtmlsnippet.php +++ b/core/src/Models/SiteHtmlsnippet.php @@ -43,6 +43,8 @@ class SiteHtmlsnippet extends Eloquent\Model use Traits\Models\ManagerActions, Traits\Models\TimeMutator; + protected $table = 'site_htmlsnippets'; + const CREATED_AT = 'createdon'; const UPDATED_AT = 'editedon'; protected $dateFormat = 'U'; diff --git a/core/src/Models/SiteModule.php b/core/src/Models/SiteModule.php index a0334a238b..363ba130fc 100644 --- a/core/src/Models/SiteModule.php +++ b/core/src/Models/SiteModule.php @@ -47,6 +47,8 @@ class SiteModule extends Eloquent\Model use Traits\Models\ManagerActions, Traits\Models\TimeMutator; + protected $table = 'site_modules'; + const CREATED_AT = 'createdon'; const UPDATED_AT = 'editedon'; protected $dateFormat = 'U'; diff --git a/core/src/Models/SitePlugin.php b/core/src/Models/SitePlugin.php index 523dba7e52..12e2f0f1a1 100644 --- a/core/src/Models/SitePlugin.php +++ b/core/src/Models/SitePlugin.php @@ -45,6 +45,8 @@ class SitePlugin extends Eloquent\Model use Traits\Models\ManagerActions, Traits\Models\TimeMutator; + protected $table = 'site_plugins'; + const CREATED_AT = 'createdon'; const UPDATED_AT = 'editedon'; protected $dateFormat = 'U'; diff --git a/core/src/Models/SitePluginEvent.php b/core/src/Models/SitePluginEvent.php index 9deec20f6e..59e5f7baf7 100644 --- a/core/src/Models/SitePluginEvent.php +++ b/core/src/Models/SitePluginEvent.php @@ -16,6 +16,8 @@ class SitePluginEvent extends Eloquent\Model { use Traits\Models\ManagerActions; + protected $table = 'site_plugin_events'; + public $incrementing = false; public $timestamps = false; diff --git a/core/src/Models/SiteSnippet.php b/core/src/Models/SiteSnippet.php index bd09a51b1a..07cf86c299 100644 --- a/core/src/Models/SiteSnippet.php +++ b/core/src/Models/SiteSnippet.php @@ -45,6 +45,8 @@ class SiteSnippet extends Eloquent\Model use Traits\Models\ManagerActions, Traits\Models\TimeMutator; + protected $table = 'site_snippets'; + const CREATED_AT = 'createdon'; const UPDATED_AT = 'editedon'; protected $dateFormat = 'U'; diff --git a/core/src/Models/SiteTemplate.php b/core/src/Models/SiteTemplate.php index 2dfdc7eccc..b3fe0f4761 100644 --- a/core/src/Models/SiteTemplate.php +++ b/core/src/Models/SiteTemplate.php @@ -46,6 +46,8 @@ class SiteTemplate extends Eloquent\Model use Traits\Models\ManagerActions, Traits\Models\TimeMutator; + protected $table = 'site_templates'; + const CREATED_AT = 'createdon'; const UPDATED_AT = 'editedon'; protected $dateFormat = 'U'; diff --git a/core/src/Models/SiteTmplvar.php b/core/src/Models/SiteTmplvar.php index c4d04f73f6..d51b7218f3 100644 --- a/core/src/Models/SiteTmplvar.php +++ b/core/src/Models/SiteTmplvar.php @@ -45,6 +45,8 @@ class SiteTmplvar extends Eloquent\Model use Traits\Models\ManagerActions, Traits\Models\TimeMutator; + protected $table = 'site_tmplvars'; + const CREATED_AT = 'createdon'; const UPDATED_AT = 'editedon'; protected $dateFormat = 'U'; diff --git a/core/src/Models/SiteTmplvarContentvalue.php b/core/src/Models/SiteTmplvarContentvalue.php index 11320eddcc..5384c1c5a3 100644 --- a/core/src/Models/SiteTmplvarContentvalue.php +++ b/core/src/Models/SiteTmplvarContentvalue.php @@ -14,6 +14,8 @@ */ class SiteTmplvarContentvalue extends Eloquent\Model { + protected $table = 'site_tmplvar_contentvalues'; + public $timestamps = false; protected $casts = [ diff --git a/core/src/Models/SiteTmplvarTemplate.php b/core/src/Models/SiteTmplvarTemplate.php index 9eb1369e8f..12af2fddf3 100644 --- a/core/src/Models/SiteTmplvarTemplate.php +++ b/core/src/Models/SiteTmplvarTemplate.php @@ -13,6 +13,8 @@ */ class SiteTmplvarTemplate extends Eloquent\Model { + protected $table = 'site_tmplvar_templates'; + public $incrementing = false; public $timestamps = false; diff --git a/core/src/Models/SystemEventname.php b/core/src/Models/SystemEventname.php index 2406ec1128..9f321cdc42 100644 --- a/core/src/Models/SystemEventname.php +++ b/core/src/Models/SystemEventname.php @@ -17,6 +17,8 @@ */ class SystemEventname extends Eloquent\Model { + protected $table = 'system_eventnames'; + public $timestamps = false; protected $casts = [ diff --git a/core/src/Models/SystemSetting.php b/core/src/Models/SystemSetting.php index 79725d472c..74cf1eae7c 100644 --- a/core/src/Models/SystemSetting.php +++ b/core/src/Models/SystemSetting.php @@ -15,6 +15,8 @@ class SystemSetting extends Eloquent\Model { use Traits\Models\ManagerActions; + protected $table = 'system_settings'; + protected $primaryKey = 'setting_name'; public $incrementing = false; public $timestamps = false; diff --git a/core/src/Models/User.php b/core/src/Models/User.php index c9dc1357bb..6236d64784 100644 --- a/core/src/Models/User.php +++ b/core/src/Models/User.php @@ -20,6 +20,8 @@ class User extends Eloquent\Model { use Traits\Models\ManagerActions; + protected $table = 'users'; + public $timestamps = false; protected $hidden = [ diff --git a/core/src/Models/UserAttribute.php b/core/src/Models/UserAttribute.php index dda5b8aeea..ed99cbd4f9 100644 --- a/core/src/Models/UserAttribute.php +++ b/core/src/Models/UserAttribute.php @@ -49,6 +49,8 @@ class UserAttribute extends Eloquent\Model { use Traits\Models\TimeMutator; + protected $table = 'user_attributes'; + const CREATED_AT = 'createdon'; const UPDATED_AT = 'editedon'; protected $dateFormat = 'U'; diff --git a/core/src/Models/UserRole.php b/core/src/Models/UserRole.php index 84c0ef7514..dcf9ef7d95 100644 --- a/core/src/Models/UserRole.php +++ b/core/src/Models/UserRole.php @@ -93,6 +93,8 @@ class UserRole extends Eloquent\Model { use Traits\Models\ManagerActions; + protected $table = 'user_roles'; + public $timestamps = false; protected $casts = [ diff --git a/core/src/Models/UserRoleVar.php b/core/src/Models/UserRoleVar.php index 2d602e5060..d20aa56ba1 100644 --- a/core/src/Models/UserRoleVar.php +++ b/core/src/Models/UserRoleVar.php @@ -15,6 +15,8 @@ */ class UserRoleVar extends Eloquent\Model { + protected $table = 'user_role_vars'; + public $incrementing = false; public $timestamps = false; diff --git a/core/src/Models/UserSetting.php b/core/src/Models/UserSetting.php index 449cde50a0..e85e225e8e 100644 --- a/core/src/Models/UserSetting.php +++ b/core/src/Models/UserSetting.php @@ -13,6 +13,8 @@ */ class UserSetting extends Eloquent\Model { + protected $table = 'user_settings'; + protected $primaryKey = null; public $incrementing = false; public $timestamps = false; diff --git a/core/src/Models/UserValue.php b/core/src/Models/UserValue.php index db2f0c868d..f9c7f679a6 100644 --- a/core/src/Models/UserValue.php +++ b/core/src/Models/UserValue.php @@ -16,6 +16,8 @@ */ class UserValue extends Model { + protected $table = 'user_values'; + public $timestamps = false; protected $casts = [ diff --git a/core/src/Traits/Models/SoftDeletes.php b/core/src/Traits/Models/SoftDeletes.php index cae753a189..2afbd8507c 100644 --- a/core/src/Traits/Models/SoftDeletes.php +++ b/core/src/Traits/Models/SoftDeletes.php @@ -1,5 +1,6 @@ $attributes + * @param array $mutatedAttributes + * @return array + */ + protected function addCastAttributesToArray(array $attributes, array $mutatedAttributes) + { + $column = $this->getDeletedAtColumn(); + if (!array_key_exists($column, $attributes) + || in_array($column, $mutatedAttributes, true) + || ($this->getCasts()[$column] ?? null) !== 'datetime' + ) { + return parent::addCastAttributesToArray($attributes, $mutatedAttributes); + } + + $raw = $attributes[$column]; + if ($raw instanceof DateTimeInterface) { + $raw = $raw->getTimestamp(); + } elseif (is_numeric($raw)) { + $raw = (int) $raw; + } + + // The parent only rewrites keys it is given, so the column goes back + // where it was and the array keeps its order. + $position = array_search($column, array_keys($attributes), true); + unset($attributes[$column]); + $attributes = parent::addCastAttributesToArray($attributes, $mutatedAttributes); + + return array_slice($attributes, 0, $position, true) + + [$column => $raw] + + array_slice($attributes, $position, null, true); + } } diff --git a/core/tests/Mocks/default_site_template_seeder_worker.php b/core/tests/Mocks/default_site_template_seeder_worker.php new file mode 100644 index 0000000000..e469dd2b8d --- /dev/null +++ b/core/tests/Mocks/default_site_template_seeder_worker.php @@ -0,0 +1,63 @@ +name] = []; + + return 0; + } + + public function insert(array $rows): void + { + DB::$inserted[$this->name] = $rows; + } + } +} + +namespace { + require $argv[1]; + + $seederClass = $argv[2]; + (new $seederClass())->run(); + + echo json_encode( + \Illuminate\Support\Facades\DB::$inserted['site_templates'][0] ?? [], + JSON_THROW_ON_ERROR + ); +} diff --git a/core/tests/Mocks/site_content_to_array_worker.php b/core/tests/Mocks/site_content_to_array_worker.php new file mode 100644 index 0000000000..ad350e5b37 --- /dev/null +++ b/core/tests/Mocks/site_content_to_array_worker.php @@ -0,0 +1,20 @@ +newFromBuilder([ + 'id' => 1, + 'pagetitle' => 'Home', + 'template' => 1, + 'createdon' => '1790625541', + 'deleted' => '0', + 'deletedon' => '0', +]); + +echo json_encode([ + 'deletedon' => $document->toArray()['deletedon'], + 'carbonLoaded' => class_exists(\Carbon\Carbon::class, false), +], JSON_THROW_ON_ERROR); diff --git a/core/tests/Unit/Install/DefaultSiteTemplateSeederTest.php b/core/tests/Unit/Install/DefaultSiteTemplateSeederTest.php new file mode 100644 index 0000000000..aed01b7523 --- /dev/null +++ b/core/tests/Unit/Install/DefaultSiteTemplateSeederTest.php @@ -0,0 +1,25 @@ +toBe(0) + ->and($template['templatesource'])->toBe('db'); + } +}); diff --git a/core/tests/Unit/Models/ModelTableNamesTest.php b/core/tests/Unit/Models/ModelTableNamesTest.php new file mode 100644 index 0000000000..e777c5c74e --- /dev/null +++ b/core/tests/Unit/Models/ModelTableNamesTest.php @@ -0,0 +1,44 @@ +isAbstract() || !$class->isSubclassOf(Model::class)) { + continue; + } + $declaring = $class->getProperty('table')->getDeclaringClass()->getName(); + $table = $class->newInstanceWithoutConstructor()->getTable(); + if ($declaring === Model::class || $table === '') { + $missing[] = $class->getShortName(); + } + } + + expect($missing)->toBe([]); +}); + +test('models that used to derive their table name keep that exact name', function () { + $models = [ + 'ActiveUser', 'ActiveUserLock', 'ActiveUserSession', 'Category', 'DocumentGroup', + 'DocumentgroupName', 'FileGroup', 'MemberGroup', 'MembergroupName', 'Permissions', + 'PermissionsGroups', 'RolePermissions', 'SiteHtmlsnippet', 'SiteModule', 'SitePlugin', + 'SitePluginEvent', 'SiteSnippet', 'SiteTemplate', 'SiteTmplvar', 'SiteTmplvarContentvalue', + 'SiteTmplvarTemplate', 'SystemEventname', 'SystemSetting', 'User', 'UserAttribute', + 'UserRole', 'UserRoleVar', 'UserSetting', 'UserValue', + ]; + + foreach ($models as $model) { + $class = new ReflectionClass('EvolutionCMS\\Models\\' . $model); + // The rule Model::getTable() applies when $table is not set. + $derived = Str::snake(Str::pluralStudly($model)); + + expect($class->newInstanceWithoutConstructor()->getTable())->toBe($derived, $model); + } +}); diff --git a/core/tests/Unit/Models/SiteContentDeletedOnArrayTest.php b/core/tests/Unit/Models/SiteContentDeletedOnArrayTest.php new file mode 100644 index 0000000000..3be3570aa8 --- /dev/null +++ b/core/tests/Unit/Models/SiteContentDeletedOnArrayTest.php @@ -0,0 +1,58 @@ +newFromBuilder($row); +} + +test('toArray keeps deletedon as the stored unix timestamp', function () { + $never = siteContentFromRow(['id' => 1, 'deletedon' => '0'])->toArray(); + $deleted = siteContentFromRow(['id' => 2, 'deletedon' => '1790625541'])->toArray(); + $missing = siteContentFromRow(['id' => 3, 'deletedon' => null])->toArray(); + + expect($never['deletedon'])->toBe(0) + ->and($deleted['deletedon'])->toBe(1790625541) + ->and($missing['deletedon'])->toBeNull(); +}); + +test('toArray keeps the column order and still casts the other attributes', function () { + $array = siteContentFromRow([ + 'id' => 1, + 'published' => '1', + 'deletedon' => '0', + 'template' => '4', + 'hidemenu' => '0', + ])->toArray(); + + expect(array_keys($array))->toBe(['id', 'published', 'deletedon', 'template', 'hidemenu']) + ->and($array['published'])->toBe(1) + ->and($array['template'])->toBe(4) + ->and($array['hidemenu'])->toBeFalse(); +}); + +test('reading deletedon as an attribute still returns a date', function () { + $document = siteContentFromRow(['id' => 1, 'deletedon' => '1790625541']); + + expect($document->deletedon)->toBeInstanceOf(Carbon::class) + ->and($document->deletedon->getTimestamp())->toBe(1790625541); +}); + +test('a deletion time set in memory is arrayed as a unix timestamp', function () { + $document = siteContentFromRow(['id' => 1, 'deletedon' => '0']); + $document->deletedon = Carbon::createFromTimestamp(1790625541); + + expect($document->toArray()['deletedon'])->toBe(1790625541); +}); + +test('arraying a document does not load Carbon', function () { + $exitCode = null; + $output = evoRunPhp(dirname(__DIR__, 2) . '/Mocks/site_content_to_array_worker.php', [], $exitCode); + $result = json_decode($output, true, flags: JSON_THROW_ON_ERROR); + + expect($exitCode)->toBe(0) + ->and($result['deletedon'])->toBe(0) + ->and($result['carbonLoaded'])->toBeFalse(); +}); diff --git a/install/stubs/seeds/install/SiteTemplatesTableSeeder.php b/install/stubs/seeds/install/SiteTemplatesTableSeeder.php index da4ee10e86..7b76098acd 100644 --- a/install/stubs/seeds/install/SiteTemplatesTableSeeder.php +++ b/install/stubs/seeds/install/SiteTemplatesTableSeeder.php @@ -18,6 +18,7 @@ public function run(): void [ 'templatename' => 'Minimal Template', 'templatealias' => '', + 'templatesource' => 'db', 'description' => 'Default minimal empty template (content returned only)', 'editor_type' => 0, 'category' => 0,