nasirkhan/laravel-starter
Laravel 13 modular starter with separated frontend/backend. Includes auth & authorization, user/role management, admin backend, backups, log viewer, and custom artisan commands (install, update, module builder). Use as a base to build reusable modules.
Date: 2026-03-21
Reviewed By: Kilo Code
Scope: Module Manager Package, Laravel-Cube Package, and Core Application Code
Verification & Fixes Applied: 2026-03-22 — All bugs cross-checked against source code; confirmed bugs fixed, false positives documented.
Improvements Applied: 2026-03-22 — Additional bugs found and fixed (#14, #15); improvement suggestions #13 (partially) and #15 (partially) applied; all 197 tests passing.
Improvements Round 2: 2026-03-22 — Suggestions #5, #6, #8, #9, #14, #19, #31, #32, #33, #34, #39 assessed and applied/resolved; remaining 29 suggestions triaged as deferred or not-applicable; 197 tests still passing.
../laravel-starter-packages/module-manager/src/ModuleManagerServiceProvider.phpif ($this->app->runningInConsole()) block was removed; $this->commands([...]) now lives directly under the outer check.$this->app->runningInConsole() on line 41, making the check on line 73 redundant../laravel-starter-packages/module-manager/src/ModuleManagerServiceProvider.phpregisterModules() already uses base_path('modules_statuses.json') correctly. The real occurrence of this bug was in ModuleBuildCommand.php (see Bug #6).File::put('modules_statuses.json', ...) — does not exist in this file../laravel-starter-packages/module-manager/src/Services/MigrationTracker.phpupdateAfterComposerUpdate() now reads module names dynamically from modules_statuses.json via base_path().$modules = ['Post', 'Category', 'Tag', 'Menu']; is hardcodedmodules_statuses.json at runtime../laravel-starter-packages/module-manager/src/Services/MigrationTracker.phpensureTrackingTableExists() now wraps Schema::create() in a try-catch that rethrows a \RuntimeException with a clear message.Schema::create() has no try-catch blockprotected function ensureTrackingTableExists(): void
{
if (! Schema::hasTable($this->trackingTable)) {
try {
Schema::create($this->trackingTable, function ($table) { ... });
} catch (\Exception $e) {
throw new \RuntimeException("Failed to create module migration tracking table: {$e->getMessage()}", 0, $e);
}
}
}
../laravel-starter-packages/module-manager/src/Services/ModuleVersion.phpgetAllVersions() now dynamically scans the Modules directory using File::directories(), automatically including all present modules.$modules = ['Post', 'Category', 'Tag', 'Menu']; is hardcodedModules directory for module names../laravel-starter-packages/module-manager/src/Commands/ModuleBuildCommand.phpenableModule() now uses base_path('modules_statuses.json'). Note: Bug #2 was misattributed to ModuleManagerServiceProvider.php; the actual occurrence was solely in this file.File::put('modules_statuses.json', ...) writes to the current working directory instead of the project rootmodules_statuses.json created in wrong directory; module not activatedFile::put(base_path('modules_statuses.json'), json_encode(array_merge(json_decode($content, true), [$moduleName => true]), JSON_PRETTY_PRINT));
app/Http/Controllers/Backend/BackendBaseController.phpreturn redirect("admin/{$module_name}"); is syntactically correct. No fix needed.app/Http/Controllers/Backend/BackendBaseController.phpapp/Http/Controllers/Backend/BackendBaseController.phpapp/Http/Controllers/Backend/BackendBaseController.phpapp/helpers.phpLog::debug(label_case($text)." | {$auth_text}"); is syntactically correct. No fix needed.app/helpers.phpgenerate_rgb_code — Generate an RGB color code string.generate_rgb_code()app/Authorizable.phpgetAbility() now null-coalesces Route::currentRouteName() and returns null early if the route name has fewer than 2 dot-separated segments.explode('.', Route::currentRouteName()) may return array with single element, causing undefined index on $routeName[1]public function getAbility($method)
{
$routeName = explode('.', Route::currentRouteName() ?? '');
$action = Arr::get($this->getAbilities(), $method);
if (count($routeName) < 2) {
return null;
}
return $action ? $action.'_'.$routeName[1] : null;
}
app/helpers.phpuser_registration() now only calls config('app.user_registration'). The redundant env() check was removed.if ((bool) env('USER_REGISTRATION')) was called directly inside the helper alongside config('app.user_registration'). Direct env() calls outside config files break php artisan config:cache.function user_registration(): bool
{
return (bool) config('app.user_registration');
}
app/helpers.phpen2bnNumber identified itself as bn2enNumberen2bnDate also identified itself as bn2enNumber with wrong description ("Convert a English number to Bengali")icon had a typo: "icon fornts" → "icon fonts"app.name and app.url are always set in Laravel's default config. Throwing on boot for missing module-manager.namespace would break fresh installs before the package config is published. Deferred; rely on runtime errors until a real misconfiguration incident occurs.HasFramework trait in laravel-cube. Deferred; current conditional approach works and adding more CSS frameworks is not an imminent need.BackendBaseController uses $request->all() which passes through mass-assignment protection via $fillable/$guarded. Form Request classes per module (see #29) remain the long-term solution.VerifyCsrfToken middleware ships in the web middleware group and protects all web routes automatically. All Blade forms using [@csrf](https://github.com/csrf) are already protected.routes/api.php file exists; the application does not expose API routes. If API routes are added in the future, apply throttle middleware at that time.SecurityHeaders middleware was created and registered, but removed as security headers are not part of default Laravel and are considered non-mandatory for this project. Can be re-added via a package like bepsvpt/secure-headers if needed later.ModuleManagerServiceProvider::registerModules() now wraps the modules_statuses.json file read in Cache::remember('module_statuses', 3600, ...). Cache is explicitly invalidated (Cache::forget('module_statuses')) in ModuleEnableCommand, ModuleDisableCommand, and ModuleBuildCommand::enableModule() after each write.BackendBaseController is a generic base; each module controller determines its own eager loading strategy. No specific N+1 issue was identified during review. Apply eager loading per-module when profiling identifies a problem.paginate(15) on admin CRUD lists. Cursor pagination is beneficial for very large datasets; apply when dataset size warrants it.tests/Feature/Unit/HelpersTest.php added with 6 assertions covering user_registration(), label_case(), encode_id()/decode_id(). Full coverage of services and commands is ongoing.// Example test
class ModuleManagerServiceProviderTest extends TestCase
{
public function test_registers_enabled_modules()
{
$provider = new ModuleManagerServiceProvider($this->app);
$provider->boot();
$this->assertTrue(class_exists(PostServiceProvider::class));
}
public function test_skips_disabled_modules()
{
// Update modules_statuses.json to disable a module
// Boot service provider
// Assert module is not registered
}
}
MigrationTracker::ensureTrackingTableExists() already wraps Schema::create() in a try-catch (fixed in Bug #4). BackendBaseController store/update/destroy/restore now use DB::transaction() which provides automatic rollback on exceptions (applied in #33).ModuleBuildCommand methods generate(), createFiles(), setFilePath(), and enableModule() now have full parameter and return type declarations.// Before:
public function getModuleData($moduleName)
{
// ...
}
// After:
public function getModuleData(string $moduleName): array
{
// ...
}
'module_migrations_tracking' and module status booleans are used in a limited, well-understood scope. A ModuleConstants class would add indirection without meaningful benefit at current scale.phpstan/phpstan as a dev dependency and a baseline config. Intentional architectural decision to defer; add when the team is ready to maintain a PHPStan baseline.MigrationTracker::updateAfterComposerUpdate() and ModuleVersion::getAllVersions() now dynamically read module names from modules_statuses.json and scan the Modules directory respectively, replacing the hardcoded ['Post', 'Category', 'Tag', 'Menu'] lists.public function getAllVersions(): array
{
$modules = $this->discoverModules();
$versions = [];
foreach ($modules as $module) {
$data = $this->getModuleData($module);
$versions[$module] = [
'version' => $data['version'] ?? 'unknown',
'description' => $data['description'] ?? '',
'keywords' => $data['keywords'] ?? [],
'priority' => $data['priority'] ?? 0,
'requires' => $data['requires'] ?? [],
];
}
return $versions;
}
protected function discoverModules(): array
{
$paths = [
base_path('Modules'),
base_path('vendor/nasirkhan/module-manager/src/Modules'),
];
$modules = [];
foreach ($paths as $path) {
if (File::exists($path)) {
$directories = File::directories($path);
foreach ($directories as $directory) {
$moduleName = basename($directory);
if (File::exists($directory.'/module.json')) {
$modules[] = $moduleName;
}
}
}
}
return array_unique($modules);
}
composer/semver as a dependency. The existing string-based version comparison is sufficient for current module versioning needs.module_migrations_tracking table tracks file-level state. Adding a full execution history table is beneficial but non-critical; defer until debugging needs justify it.BackendBaseController::store() and update() currently use $request->all(). Each module controller should have its own FormRequest class. This is a broad change requiring a Form Request per module per operation. Deferring as a dedicated task.routes/api.php exists; the application has no API routes. Apply if an API layer is added.logUserAccess() in app/helpers.php now passes a structured context array to Log::debug() containing user_id, user_name, ip, url, and method. Previously it concatenated these into the log message string./up health check endpoint is registered in bootstrap/app.php via health: '/up'. This covers the basic liveness probe.BackendBaseController::store(), update(), destroy(), and restore() now wrap their write operations in DB::transaction(). The restore() method also now uses findOrFail() instead of find() for consistency.BackendBaseController already implements trashed() and restore() methods, confirming soft deletes are in use across modules. The destroy() method calls delete() (soft delete) not forceDelete().boot() validation via exceptions is an unconventional approach in Laravel; the correct pattern is Form Request classes (see #29). Defer as part of the Form Request task.barryvdh/laravel-ide-helper is a useful dev dependency but requires approval to add. Defer until the team confirms IDE helper generation is wanted.How can I help you explore Laravel packages today?