A hotreload
This commit is contained in:
parent
c76e6ae111
commit
935bfb28d3
|
|
@ -2,6 +2,18 @@
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Hot Reload (Development Mode)
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When enabled, controller files are reloaded on every request instead of
|
||||||
|
| being cached. This allows you to make code changes without restarting
|
||||||
|
| the WebSocket server. Disable in production for better performance.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'hot_reload' => env('WEBSOCKET_HOT_RELOAD', env('APP_DEBUG', false)),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Dashboard Settings
|
| Dashboard Settings
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ namespace BlaxSoftware\LaravelWebSockets\Websocket;
|
||||||
* - Kebab-case: `admin-user.method` → `AdminUserController`
|
* - Kebab-case: `admin-user.method` → `AdminUserController`
|
||||||
* - Folder structure: `admin-user.method` → `Admin/UserController` (fuzzy)
|
* - Folder structure: `admin-user.method` → `Admin/UserController` (fuzzy)
|
||||||
* - Dynamic discovery: new controllers are found and cached at runtime
|
* - Dynamic discovery: new controllers are found and cached at runtime
|
||||||
|
* - Hot reload: disable caching in dev mode for instant code updates
|
||||||
*/
|
*/
|
||||||
class ControllerResolver
|
class ControllerResolver
|
||||||
{
|
{
|
||||||
|
|
@ -36,6 +37,19 @@ class ControllerResolver
|
||||||
*/
|
*/
|
||||||
private static bool $scanned = false;
|
private static bool $scanned = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hot reload mode - when enabled, caching is disabled for development
|
||||||
|
*/
|
||||||
|
private static ?bool $hotReload = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track when classes were loaded to detect stale code
|
||||||
|
* Maps class name => file mtime at load time
|
||||||
|
*
|
||||||
|
* @var array<string, int>
|
||||||
|
*/
|
||||||
|
private static array $classLoadTimes = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App controller namespace
|
* App controller namespace
|
||||||
*/
|
*/
|
||||||
|
|
@ -46,6 +60,17 @@ class ControllerResolver
|
||||||
*/
|
*/
|
||||||
private const VENDOR_NAMESPACE = '\\BlaxSoftware\\LaravelWebSockets\\Websocket\\Controllers\\';
|
private const VENDOR_NAMESPACE = '\\BlaxSoftware\\LaravelWebSockets\\Websocket\\Controllers\\';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if hot reload mode is enabled
|
||||||
|
*/
|
||||||
|
private static function isHotReload(): bool
|
||||||
|
{
|
||||||
|
if (self::$hotReload === null) {
|
||||||
|
self::$hotReload = (bool) config('websockets.hot_reload', false);
|
||||||
|
}
|
||||||
|
return self::$hotReload;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve controller class for an event prefix
|
* Resolve controller class for an event prefix
|
||||||
*
|
*
|
||||||
|
|
@ -54,6 +79,11 @@ class ControllerResolver
|
||||||
*/
|
*/
|
||||||
public static function resolve(string $eventPrefix): ?string
|
public static function resolve(string $eventPrefix): ?string
|
||||||
{
|
{
|
||||||
|
// In hot reload mode, skip cache and invalidate opcache for fresh code
|
||||||
|
if (self::isHotReload()) {
|
||||||
|
return self::resolveWithHotReload($eventPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
// Check cache first (O(1) lookup)
|
// Check cache first (O(1) lookup)
|
||||||
if (array_key_exists($eventPrefix, self::$controllerCache)) {
|
if (array_key_exists($eventPrefix, self::$controllerCache)) {
|
||||||
return self::$controllerCache[$eventPrefix];
|
return self::$controllerCache[$eventPrefix];
|
||||||
|
|
@ -68,6 +98,97 @@ class ControllerResolver
|
||||||
return $controllerClass;
|
return $controllerClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve controller with hot reload - invalidates opcache for fresh code
|
||||||
|
* This is slower but allows code changes without server restart
|
||||||
|
*/
|
||||||
|
private static function resolveWithHotReload(string $eventPrefix): ?string
|
||||||
|
{
|
||||||
|
$directName = self::kebabToPascal($eventPrefix) . 'Controller';
|
||||||
|
|
||||||
|
// Try app namespace first
|
||||||
|
$appClass = self::APP_NAMESPACE . $directName;
|
||||||
|
$appFile = self::getControllerFilePath($appClass);
|
||||||
|
|
||||||
|
if ($appFile && file_exists($appFile)) {
|
||||||
|
self::invalidateAndReload($appFile);
|
||||||
|
if (class_exists($appClass, true)) {
|
||||||
|
return $appClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try vendor namespace
|
||||||
|
$vendorClass = self::VENDOR_NAMESPACE . $directName;
|
||||||
|
$vendorFile = self::getControllerFilePath($vendorClass);
|
||||||
|
|
||||||
|
if ($vendorFile && file_exists($vendorFile)) {
|
||||||
|
self::invalidateAndReload($vendorFile);
|
||||||
|
if (class_exists($vendorClass, true)) {
|
||||||
|
return $vendorClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try folder structure for kebab-case names
|
||||||
|
$parts = explode('-', $eventPrefix);
|
||||||
|
if (count($parts) > 1) {
|
||||||
|
for ($folderDepth = count($parts) - 1; $folderDepth >= 1; $folderDepth--) {
|
||||||
|
$folderParts = array_slice($parts, 0, $folderDepth);
|
||||||
|
$nameParts = array_slice($parts, $folderDepth);
|
||||||
|
|
||||||
|
$folder = implode('/', array_map('ucfirst', $folderParts));
|
||||||
|
$name = implode('', array_map('ucfirst', $nameParts)) . 'Controller';
|
||||||
|
|
||||||
|
// Try app namespace with folder
|
||||||
|
$appClass = self::APP_NAMESPACE . str_replace('/', '\\', $folder) . '\\' . $name;
|
||||||
|
$appFile = self::getControllerFilePath($appClass);
|
||||||
|
|
||||||
|
if ($appFile && file_exists($appFile)) {
|
||||||
|
self::invalidateAndReload($appFile);
|
||||||
|
if (class_exists($appClass, true)) {
|
||||||
|
return $appClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate opcache for a file and force reload
|
||||||
|
*/
|
||||||
|
private static function invalidateAndReload(string $filePath): void
|
||||||
|
{
|
||||||
|
if (function_exists('opcache_invalidate')) {
|
||||||
|
opcache_invalidate($filePath, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the file path for a controller class
|
||||||
|
*/
|
||||||
|
private static function getControllerFilePath(string $className): ?string
|
||||||
|
{
|
||||||
|
// For App namespace
|
||||||
|
if (str_starts_with($className, self::APP_NAMESPACE)) {
|
||||||
|
$relativePath = str_replace(self::APP_NAMESPACE, '', $className);
|
||||||
|
$relativePath = str_replace('\\', '/', $relativePath);
|
||||||
|
$appPath = self::getAppControllersPath();
|
||||||
|
if ($appPath) {
|
||||||
|
return $appPath . '/' . $relativePath . '.php';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For vendor namespace
|
||||||
|
if (str_starts_with($className, self::VENDOR_NAMESPACE)) {
|
||||||
|
$relativePath = str_replace(self::VENDOR_NAMESPACE, '', $className);
|
||||||
|
$relativePath = str_replace('\\', '/', $relativePath);
|
||||||
|
return __DIR__ . '/Controllers/' . $relativePath . '.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find controller using multiple strategies
|
* Find controller using multiple strategies
|
||||||
* Optimized for speed: most common case (direct match) checked first
|
* Optimized for speed: most common case (direct match) checked first
|
||||||
|
|
@ -254,12 +375,13 @@ class ControllerResolver
|
||||||
self::$controllerCache = [];
|
self::$controllerCache = [];
|
||||||
self::$availableControllers = [];
|
self::$availableControllers = [];
|
||||||
self::$scanned = false;
|
self::$scanned = false;
|
||||||
|
self::$hotReload = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get cache statistics (for debugging)
|
* Get cache statistics (for debugging)
|
||||||
*
|
*
|
||||||
* @return array{cached: int, available: int, scanned: bool}
|
* @return array{cached: int, available: int, scanned: bool, hot_reload: bool}
|
||||||
*/
|
*/
|
||||||
public static function getStats(): array
|
public static function getStats(): array
|
||||||
{
|
{
|
||||||
|
|
@ -267,6 +389,7 @@ class ControllerResolver
|
||||||
'cached' => count(self::$controllerCache),
|
'cached' => count(self::$controllerCache),
|
||||||
'available' => count(self::$availableControllers),
|
'available' => count(self::$availableControllers),
|
||||||
'scanned' => self::$scanned,
|
'scanned' => self::$scanned,
|
||||||
|
'hot_reload' => self::isHotReload(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue