new modules

This commit is contained in:
Mark Gregory
2026-07-24 09:25:27 +01:00
parent d7d2a3f610
commit 73f7a918c1
88 changed files with 2342 additions and 1 deletions
+6
View File
@@ -0,0 +1,6 @@
<?php
return [
'name' => 'Administration',
'enabled' => true,
];
@@ -0,0 +1,21 @@
<?php
namespace Modules\Administration\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\Administration\Support\ModuleInfo;
final class AdministrationServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->register(RouteServiceProvider::class);
$this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower());
}
public function boot(): void
{
$this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
$this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower());
}
}
@@ -0,0 +1,17 @@
<?php
namespace Modules\Administration\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
final class RouteServiceProvider extends ServiceProvider
{
public function boot(): void
{
parent::boot();
$this->routes(function (): void {
Route::middleware('web')->group(module_path('Administration', 'Routes/web.php'));
});
}
}
+5
View File
@@ -0,0 +1,5 @@
<?php
use Illuminate\Support\Facades\Route;
// Module routes are intentionally registered by each feature as it is enabled.
@@ -0,0 +1,9 @@
<?php
namespace Modules\Administration\Support;
final class ModuleInfo
{
public static function name(): string { return 'Administration'; }
public static function nameLower(): string { return 'administration'; }
}
@@ -0,0 +1,5 @@
<?php
it('boots the Administration module', function (): void {
expect(config('administration.name'))->toBe('Administration');
});
+10
View File
@@ -0,0 +1,10 @@
{
"name": "absdev/administration-module",
"description": "Administration foundation module for the ABSDEV Laravel application platform.",
"type": "laravel-module",
"autoload": {
"psr-4": {
"Modules\\Administration\\": ""
}
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "Administration",
"alias": "administration",
"version": "1.0.0",
"description": "Administration foundation module for the ABSDEV Laravel application platform.",
"keywords": [
"laravel",
"modules",
"administration"
],
"priority": 10,
"providers": [
"Modules\\Administration\\Providers\\AdministrationServiceProvider"
],
"aliases": {},
"files": [],
"requires": [
"Core"
]
}
+6
View File
@@ -0,0 +1,6 @@
<?php
return [
'name' => 'Audit',
'enabled' => true,
];
@@ -0,0 +1,3 @@
<?php
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema;
return new class extends Migration { public function up(): void { Schema::create('audit_logs', function(Blueprint $table): void { $table->id(); $table->nullableMorphs('actor'); $table->string('event')->index(); $table->nullableMorphs('auditable'); $table->json('old_values')->nullable(); $table->json('new_values')->nullable(); $table->string('ip_address',45)->nullable(); $table->text('user_agent')->nullable(); $table->timestamp('created_at')->useCurrent(); }); } public function down(): void { Schema::dropIfExists('audit_logs'); } };
+3
View File
@@ -0,0 +1,3 @@
<?php
namespace Modules\Audit\Models; use Illuminate\Database\Eloquent\Model;
final class AuditLog extends Model { public $timestamps=false; protected $guarded=[]; protected function casts(): array { return ['old_values'=>'array','new_values'=>'array','created_at'=>'datetime']; } }
@@ -0,0 +1,21 @@
<?php
namespace Modules\Audit\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\Audit\Support\ModuleInfo;
final class AuditServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->register(RouteServiceProvider::class);
$this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower());
}
public function boot(): void
{
$this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
$this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower());
}
}
@@ -0,0 +1,17 @@
<?php
namespace Modules\Audit\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
final class RouteServiceProvider extends ServiceProvider
{
public function boot(): void
{
parent::boot();
$this->routes(function (): void {
Route::middleware('web')->group(module_path('Audit', 'Routes/web.php'));
});
}
}
+5
View File
@@ -0,0 +1,5 @@
<?php
use Illuminate\Support\Facades\Route;
// Module routes are intentionally registered by each feature as it is enabled.
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace Modules\Audit\Support;
final class ModuleInfo
{
public static function name(): string { return 'Audit'; }
public static function nameLower(): string { return 'audit'; }
}
@@ -0,0 +1,5 @@
<?php
it('boots the Audit module', function (): void {
expect(config('audit.name'))->toBe('Audit');
});
+10
View File
@@ -0,0 +1,10 @@
{
"name": "absdev/audit-module",
"description": "Audit foundation module for the ABSDEV Laravel application platform.",
"type": "laravel-module",
"autoload": {
"psr-4": {
"Modules\\Audit\\": ""
}
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "Audit",
"alias": "audit",
"version": "1.0.0",
"description": "Audit foundation module for the ABSDEV Laravel application platform.",
"keywords": [
"laravel",
"modules",
"audit"
],
"priority": 10,
"providers": [
"Modules\\Audit\\Providers\\AuditServiceProvider"
],
"aliases": {},
"files": [],
"requires": [
"Core"
]
}
+6
View File
@@ -0,0 +1,6 @@
<?php
return [
'name' => 'Core',
'enabled' => true,
];
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace Modules\Core\Contracts;
interface ModuleRegistry
{
/** @return array<int, array{name:string, alias:string, version:string, enabled:bool}> */
public function all(): array;
public function enabled(string $name): bool;
}
@@ -0,0 +1,24 @@
<?php
namespace Modules\Core\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\Core\Support\ModuleInfo;
use Modules\Core\Contracts\ModuleRegistry;
use Modules\Core\Services\LaravelModuleRegistry;
final class CoreServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->register(RouteServiceProvider::class);
$this->app->singleton(ModuleRegistry::class, LaravelModuleRegistry::class);
$this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower());
}
public function boot(): void
{
$this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
$this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower());
}
}
@@ -0,0 +1,17 @@
<?php
namespace Modules\Core\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
final class RouteServiceProvider extends ServiceProvider
{
public function boot(): void
{
parent::boot();
$this->routes(function (): void {
Route::middleware('web')->group(module_path('Core', 'Routes/web.php'));
});
}
}
+5
View File
@@ -0,0 +1,5 @@
<?php
use Illuminate\Support\Facades\Route;
// Module routes are intentionally registered by each feature as it is enabled.
@@ -0,0 +1,34 @@
<?php
namespace Modules\Core\Services;
use Modules\Core\Contracts\ModuleRegistry;
final class LaravelModuleRegistry implements ModuleRegistry
{
public function all(): array
{
$statuses = json_decode((string) @file_get_contents(base_path('modules_statuses.json')), true) ?: [];
$modules = [];
foreach (glob(base_path('Modules/*/module.json')) ?: [] as $manifest) {
$data = json_decode((string) file_get_contents($manifest), true) ?: [];
$name = (string) ($data['name'] ?? basename(dirname($manifest)));
$modules[] = [
'name' => $name,
'alias' => (string) ($data['alias'] ?? strtolower($name)),
'version' => (string) ($data['version'] ?? '0.0.0'),
'enabled' => (bool) ($statuses[$name] ?? false),
];
}
usort($modules, fn (array $a, array $b): int => $a['name'] <=> $b['name']);
return $modules;
}
public function enabled(string $name): bool
{
foreach ($this->all() as $module) {
if (strcasecmp($module['name'], $name) === 0) return $module['enabled'];
}
return false;
}
}
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace Modules\Core\Support;
final class ModuleInfo
{
public static function name(): string { return 'Core'; }
public static function nameLower(): string { return 'core'; }
}
@@ -0,0 +1,5 @@
<?php
it('boots the Core module', function (): void {
expect(config('core.name'))->toBe('Core');
});
+10
View File
@@ -0,0 +1,10 @@
{
"name": "absdev/core-module",
"description": "Core foundation module for the ABSDEV Laravel application platform.",
"type": "laravel-module",
"autoload": {
"psr-4": {
"Modules\\Core\\": ""
}
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"name": "Core",
"alias": "core",
"version": "1.0.0",
"description": "Core foundation module for the ABSDEV Laravel application platform.",
"keywords": [
"laravel",
"modules",
"core"
],
"priority": 100,
"providers": [
"Modules\\Core\\Providers\\CoreServiceProvider"
],
"aliases": {},
"files": [],
"requires": []
}
+6
View File
@@ -0,0 +1,6 @@
<?php
return [
'name' => 'Media',
'enabled' => true,
];
@@ -0,0 +1,3 @@
<?php
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema;
return new class extends Migration { public function up(): void { Schema::create('media', function(Blueprint $table): void { $table->id(); $table->uuid('uuid')->unique(); $table->nullableMorphs('attachable'); $table->string('collection')->default('default')->index(); $table->string('disk')->default('public'); $table->string('path'); $table->string('original_name'); $table->string('mime_type')->nullable(); $table->unsignedBigInteger('size')->default(0); $table->string('title')->nullable(); $table->string('alt_text')->nullable(); $table->text('caption')->nullable(); $table->json('metadata')->nullable(); $table->unsignedInteger('sort_order')->default(0); $table->foreignId('uploaded_by')->nullable()->constrained('users')->nullOnDelete(); $table->timestamps(); $table->softDeletes(); $table->index(['attachable_type','attachable_id','collection','sort_order'],'media_attachable_collection_index'); }); } public function down(): void { Schema::dropIfExists('media'); } };
+3
View File
@@ -0,0 +1,3 @@
<?php
namespace Modules\Media\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Str;
final class Media extends Model { use SoftDeletes; protected $table='media'; protected $guarded=[]; protected static function booted(): void { static::creating(fn(self $m) => $m->uuid ??= (string) Str::uuid()); } protected function casts(): array { return ['metadata'=>'array','size'=>'integer','sort_order'=>'integer']; } }
@@ -0,0 +1,21 @@
<?php
namespace Modules\Media\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\Media\Support\ModuleInfo;
final class MediaServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->register(RouteServiceProvider::class);
$this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower());
}
public function boot(): void
{
$this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
$this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower());
}
}
@@ -0,0 +1,17 @@
<?php
namespace Modules\Media\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
final class RouteServiceProvider extends ServiceProvider
{
public function boot(): void
{
parent::boot();
$this->routes(function (): void {
Route::middleware('web')->group(module_path('Media', 'Routes/web.php'));
});
}
}
+5
View File
@@ -0,0 +1,5 @@
<?php
use Illuminate\Support\Facades\Route;
// Module routes are intentionally registered by each feature as it is enabled.
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace Modules\Media\Support;
final class ModuleInfo
{
public static function name(): string { return 'Media'; }
public static function nameLower(): string { return 'media'; }
}
@@ -0,0 +1,5 @@
<?php
it('boots the Media module', function (): void {
expect(config('media.name'))->toBe('Media');
});
+10
View File
@@ -0,0 +1,10 @@
{
"name": "absdev/media-module",
"description": "Media foundation module for the ABSDEV Laravel application platform.",
"type": "laravel-module",
"autoload": {
"psr-4": {
"Modules\\Media\\": ""
}
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "Media",
"alias": "media",
"version": "1.0.0",
"description": "Media foundation module for the ABSDEV Laravel application platform.",
"keywords": [
"laravel",
"modules",
"media"
],
"priority": 10,
"providers": [
"Modules\\Media\\Providers\\MediaServiceProvider"
],
"aliases": {},
"files": [],
"requires": [
"Core"
]
}
+6
View File
@@ -0,0 +1,6 @@
<?php
return [
'name' => 'Roles',
'enabled' => true,
];
@@ -0,0 +1,119 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
$modelMorphKey = $columnNames['model_morph_key'] ?? 'model_id';
throw_if(
empty($tableNames),
RuntimeException::class,
'The Spatie permission configuration could not be loaded.'
);
Schema::create($tableNames['permissions'], function (Blueprint $table): void {
$table->id();
$table->string('name');
$table->string('guard_name');
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
Schema::create($tableNames['roles'], function (Blueprint $table): void {
$table->id();
$table->string('name');
$table->string('guard_name');
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use (
$tableNames,
$pivotPermission,
$modelMorphKey
): void {
$table->unsignedBigInteger($pivotPermission);
$table->string('model_type');
$table->unsignedBigInteger($modelMorphKey);
$table->index([$modelMorphKey, 'model_type'], 'model_has_permissions_model_id_model_type_index');
$table->foreign($pivotPermission)
->references('id')
->on($tableNames['permissions'])
->cascadeOnDelete();
$table->primary(
[$pivotPermission, $modelMorphKey, 'model_type'],
'model_has_permissions_permission_model_type_primary'
);
});
Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use (
$tableNames,
$pivotRole,
$modelMorphKey
): void {
$table->unsignedBigInteger($pivotRole);
$table->string('model_type');
$table->unsignedBigInteger($modelMorphKey);
$table->index([$modelMorphKey, 'model_type'], 'model_has_roles_model_id_model_type_index');
$table->foreign($pivotRole)
->references('id')
->on($tableNames['roles'])
->cascadeOnDelete();
$table->primary(
[$pivotRole, $modelMorphKey, 'model_type'],
'model_has_roles_role_model_type_primary'
);
});
Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use (
$tableNames,
$pivotRole,
$pivotPermission
): void {
$table->unsignedBigInteger($pivotPermission);
$table->unsignedBigInteger($pivotRole);
$table->foreign($pivotPermission)
->references('id')
->on($tableNames['permissions'])
->cascadeOnDelete();
$table->foreign($pivotRole)
->references('id')
->on($tableNames['roles'])
->cascadeOnDelete();
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
});
app('cache')->store(config('permission.cache.store') === 'default'
? null
: config('permission.cache.store'))
->forget(config('permission.cache.key'));
}
public function down(): void
{
$tableNames = config('permission.table_names');
Schema::dropIfExists($tableNames['role_has_permissions']);
Schema::dropIfExists($tableNames['model_has_roles']);
Schema::dropIfExists($tableNames['model_has_permissions']);
Schema::dropIfExists($tableNames['roles']);
Schema::dropIfExists($tableNames['permissions']);
}
};
@@ -0,0 +1,44 @@
<?php
namespace Modules\Roles\Database\Seeders;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Spatie\Permission\PermissionRegistrar;
final class RolePermissionSeeder extends Seeder
{
public function run(): void
{
$registrar = app(PermissionRegistrar::class);
$registrar->forgetCachedPermissions();
$permissions = [
'administration.access',
'users.view',
'users.create',
'users.update',
'users.delete',
'roles.manage',
'settings.manage',
'audit.view',
'media.view',
'media.upload',
'media.update',
'media.delete',
'seo.manage',
'search.manage',
'themes.manage',
];
foreach ($permissions as $permission) {
Permission::findOrCreate($permission, 'web');
}
$administrator = Role::findOrCreate('admin', 'web');
$administrator->syncPermissions($permissions);
$registrar->forgetCachedPermissions();
}
}
@@ -0,0 +1,21 @@
<?php
namespace Modules\Roles\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\Roles\Support\ModuleInfo;
final class RolesServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->register(RouteServiceProvider::class);
$this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower());
}
public function boot(): void
{
$this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
$this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower());
}
}
@@ -0,0 +1,17 @@
<?php
namespace Modules\Roles\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
final class RouteServiceProvider extends ServiceProvider
{
public function boot(): void
{
parent::boot();
$this->routes(function (): void {
Route::middleware('web')->group(module_path('Roles', 'Routes/web.php'));
});
}
}
+5
View File
@@ -0,0 +1,5 @@
<?php
use Illuminate\Support\Facades\Route;
// Module routes are intentionally registered by each feature as it is enabled.
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace Modules\Roles\Support;
final class ModuleInfo
{
public static function name(): string { return 'Roles'; }
public static function nameLower(): string { return 'roles'; }
}
@@ -0,0 +1,5 @@
<?php
it('boots the Roles module', function (): void {
expect(config('roles.name'))->toBe('Roles');
});
+10
View File
@@ -0,0 +1,10 @@
{
"name": "absdev/roles-module",
"description": "Roles foundation module for the ABSDEV Laravel application platform.",
"type": "laravel-module",
"autoload": {
"psr-4": {
"Modules\\Roles\\": ""
}
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "Roles",
"alias": "roles",
"version": "1.0.0",
"description": "Roles foundation module for the ABSDEV Laravel application platform.",
"keywords": [
"laravel",
"modules",
"roles"
],
"priority": 10,
"providers": [
"Modules\\Roles\\Providers\\RolesServiceProvider"
],
"aliases": {},
"files": [],
"requires": [
"Core"
]
}
+6
View File
@@ -0,0 +1,6 @@
<?php
return [
'name' => 'Search',
'enabled' => true,
];
@@ -0,0 +1,3 @@
<?php
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema;
return new class extends Migration { public function up(): void { Schema::create('search_index', function(Blueprint $table): void { $table->id(); $table->morphs('searchable'); $table->string('title')->index(); $table->text('summary')->nullable(); $table->longText('content')->nullable(); $table->string('url')->nullable(); $table->json('metadata')->nullable(); $table->boolean('is_public')->default(true)->index(); $table->timestamp('published_at')->nullable()->index(); $table->timestamps(); $table->unique(['searchable_type','searchable_id']); }); } public function down(): void { Schema::dropIfExists('search_index'); } };
@@ -0,0 +1,17 @@
<?php
namespace Modules\Search\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
final class RouteServiceProvider extends ServiceProvider
{
public function boot(): void
{
parent::boot();
$this->routes(function (): void {
Route::middleware('web')->group(module_path('Search', 'Routes/web.php'));
});
}
}
@@ -0,0 +1,21 @@
<?php
namespace Modules\Search\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\Search\Support\ModuleInfo;
final class SearchServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->register(RouteServiceProvider::class);
$this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower());
}
public function boot(): void
{
$this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
$this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower());
}
}
+5
View File
@@ -0,0 +1,5 @@
<?php
use Illuminate\Support\Facades\Route;
// Module routes are intentionally registered by each feature as it is enabled.
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace Modules\Search\Support;
final class ModuleInfo
{
public static function name(): string { return 'Search'; }
public static function nameLower(): string { return 'search'; }
}
@@ -0,0 +1,5 @@
<?php
it('boots the Search module', function (): void {
expect(config('search.name'))->toBe('Search');
});
+10
View File
@@ -0,0 +1,10 @@
{
"name": "absdev/search-module",
"description": "Search foundation module for the ABSDEV Laravel application platform.",
"type": "laravel-module",
"autoload": {
"psr-4": {
"Modules\\Search\\": ""
}
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "Search",
"alias": "search",
"version": "1.0.0",
"description": "Search foundation module for the ABSDEV Laravel application platform.",
"keywords": [
"laravel",
"modules",
"search"
],
"priority": 10,
"providers": [
"Modules\\Search\\Providers\\SearchServiceProvider"
],
"aliases": {},
"files": [],
"requires": [
"Core"
]
}
+6
View File
@@ -0,0 +1,6 @@
<?php
return [
'name' => 'Seo',
'enabled' => true,
];
@@ -0,0 +1,3 @@
<?php
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema;
return new class extends Migration { public function up(): void { Schema::create('seo_metadata', function(Blueprint $table): void { $table->id(); $table->morphs('seoable'); $table->string('title')->nullable(); $table->text('description')->nullable(); $table->string('canonical_url')->nullable(); $table->string('robots')->default('index,follow'); $table->string('og_title')->nullable(); $table->text('og_description')->nullable(); $table->string('og_image')->nullable(); $table->string('twitter_card')->default('summary_large_image'); $table->string('schema_type')->nullable(); $table->json('schema_data')->nullable(); $table->boolean('include_in_sitemap')->default(true); $table->decimal('sitemap_priority',2,1)->default(0.5); $table->string('sitemap_change_frequency')->default('monthly'); $table->timestamps(); $table->unique(['seoable_type','seoable_id']); }); } public function down(): void { Schema::dropIfExists('seo_metadata'); } };
+3
View File
@@ -0,0 +1,3 @@
<?php
namespace Modules\Seo\Models; use Illuminate\Database\Eloquent\Model;
final class SeoMetadata extends Model { protected $table='seo_metadata'; protected $guarded=[]; protected function casts(): array { return ['schema_data'=>'array','include_in_sitemap'=>'boolean','sitemap_priority'=>'decimal:1']; } }
@@ -0,0 +1,17 @@
<?php
namespace Modules\Seo\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
final class RouteServiceProvider extends ServiceProvider
{
public function boot(): void
{
parent::boot();
$this->routes(function (): void {
Route::middleware('web')->group(module_path('Seo', 'Routes/web.php'));
});
}
}
@@ -0,0 +1,21 @@
<?php
namespace Modules\Seo\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\Seo\Support\ModuleInfo;
final class SeoServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->register(RouteServiceProvider::class);
$this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower());
}
public function boot(): void
{
$this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
$this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower());
}
}
+5
View File
@@ -0,0 +1,5 @@
<?php
use Illuminate\Support\Facades\Route;
// Module routes are intentionally registered by each feature as it is enabled.
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace Modules\Seo\Support;
final class ModuleInfo
{
public static function name(): string { return 'Seo'; }
public static function nameLower(): string { return 'seo'; }
}
@@ -0,0 +1,5 @@
<?php
it('boots the Seo module', function (): void {
expect(config('seo.name'))->toBe('Seo');
});
+10
View File
@@ -0,0 +1,10 @@
{
"name": "absdev/seo-module",
"description": "Seo foundation module for the ABSDEV Laravel application platform.",
"type": "laravel-module",
"autoload": {
"psr-4": {
"Modules\\Seo\\": ""
}
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "Seo",
"alias": "seo",
"version": "1.0.0",
"description": "Seo foundation module for the ABSDEV Laravel application platform.",
"keywords": [
"laravel",
"modules",
"seo"
],
"priority": 10,
"providers": [
"Modules\\Seo\\Providers\\SeoServiceProvider"
],
"aliases": {},
"files": [],
"requires": [
"Core"
]
}
+6
View File
@@ -0,0 +1,6 @@
<?php
return [
'name' => 'Settings',
'enabled' => true,
];
@@ -0,0 +1,16 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('settings', function (Blueprint $table): void {
$table->id(); $table->string('group')->default('general')->index(); $table->string('key');
$table->longText('value')->nullable(); $table->string('type')->default('string'); $table->boolean('is_public')->default(false);
$table->timestamps(); $table->unique(['group','key']);
});
}
public function down(): void { Schema::dropIfExists('settings'); }
};
+11
View File
@@ -0,0 +1,11 @@
<?php
namespace Modules\Settings\Models;
use Illuminate\Database\Eloquent\Model;
final class Setting extends Model
{
protected $fillable = ['group','key','value','type','is_public'];
protected function casts(): array { return ['is_public'=>'boolean']; }
}
@@ -0,0 +1,17 @@
<?php
namespace Modules\Settings\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
final class RouteServiceProvider extends ServiceProvider
{
public function boot(): void
{
parent::boot();
$this->routes(function (): void {
Route::middleware('web')->group(module_path('Settings', 'Routes/web.php'));
});
}
}
@@ -0,0 +1,21 @@
<?php
namespace Modules\Settings\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\Settings\Support\ModuleInfo;
final class SettingsServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->register(RouteServiceProvider::class);
$this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower());
}
public function boot(): void
{
$this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
$this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower());
}
}
+5
View File
@@ -0,0 +1,5 @@
<?php
use Illuminate\Support\Facades\Route;
// Module routes are intentionally registered by each feature as it is enabled.
@@ -0,0 +1,17 @@
<?php
namespace Modules\Settings\Services;
use Illuminate\Support\Facades\Cache;
use Modules\Settings\Models\Setting;
final class SettingsRepository
{
public function get(string $key, mixed $default = null, string $group = 'general'): mixed
{ return Cache::rememberForever("settings.$group.$key", fn () => Setting::query()->where(compact('group','key'))->value('value') ?? $default); }
public function put(string $key, mixed $value, string $group = 'general', string $type = 'string', bool $public = false): Setting
{
$setting = Setting::query()->updateOrCreate(compact('group','key'), ['value'=>is_scalar($value)||$value===null?(string)$value:json_encode($value, JSON_THROW_ON_ERROR),'type'=>$type,'is_public'=>$public]);
Cache::forget("settings.$group.$key"); return $setting;
}
}
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace Modules\Settings\Support;
final class ModuleInfo
{
public static function name(): string { return 'Settings'; }
public static function nameLower(): string { return 'settings'; }
}
@@ -0,0 +1,5 @@
<?php
it('boots the Settings module', function (): void {
expect(config('settings.name'))->toBe('Settings');
});
+10
View File
@@ -0,0 +1,10 @@
{
"name": "absdev/settings-module",
"description": "Settings foundation module for the ABSDEV Laravel application platform.",
"type": "laravel-module",
"autoload": {
"psr-4": {
"Modules\\Settings\\": ""
}
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "Settings",
"alias": "settings",
"version": "1.0.0",
"description": "Settings foundation module for the ABSDEV Laravel application platform.",
"keywords": [
"laravel",
"modules",
"settings"
],
"priority": 10,
"providers": [
"Modules\\Settings\\Providers\\SettingsServiceProvider"
],
"aliases": {},
"files": [],
"requires": [
"Core"
]
}
+6
View File
@@ -0,0 +1,6 @@
<?php
return [
'name' => 'Users',
'enabled' => true,
];
@@ -0,0 +1,17 @@
<?php
namespace Modules\Users\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
final class RouteServiceProvider extends ServiceProvider
{
public function boot(): void
{
parent::boot();
$this->routes(function (): void {
Route::middleware('web')->group(module_path('Users', 'Routes/web.php'));
});
}
}
@@ -0,0 +1,21 @@
<?php
namespace Modules\Users\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\Users\Support\ModuleInfo;
final class UsersServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->register(RouteServiceProvider::class);
$this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower());
}
public function boot(): void
{
$this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
$this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower());
}
}
+5
View File
@@ -0,0 +1,5 @@
<?php
use Illuminate\Support\Facades\Route;
// Module routes are intentionally registered by each feature as it is enabled.
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace Modules\Users\Support;
final class ModuleInfo
{
public static function name(): string { return 'Users'; }
public static function nameLower(): string { return 'users'; }
}
@@ -0,0 +1,5 @@
<?php
it('boots the Users module', function (): void {
expect(config('users.name'))->toBe('Users');
});
+10
View File
@@ -0,0 +1,10 @@
{
"name": "absdev/users-module",
"description": "Users foundation module for the ABSDEV Laravel application platform.",
"type": "laravel-module",
"autoload": {
"psr-4": {
"Modules\\Users\\": ""
}
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "Users",
"alias": "users",
"version": "1.0.0",
"description": "Users foundation module for the ABSDEV Laravel application platform.",
"keywords": [
"laravel",
"modules",
"users"
],
"priority": 10,
"providers": [
"Modules\\Users\\Providers\\UsersServiceProvider"
],
"aliases": {},
"files": [],
"requires": [
"Core"
]
}
+10 -1
View File
@@ -1,3 +1,12 @@
{
"Stylist": true
"Stylist": true,
"Core": true,
"Administration": true,
"Users": true,
"Roles": true,
"Settings": true,
"Audit": true,
"Media": true,
"Seo": true,
"Search": true
}
File diff suppressed because it is too large Load Diff