Created
May 13, 2024 14:53
-
-
Save midhunmonachan/1b602059b3da42fdace118cac9452787 to your computer and use it in GitHub Desktop.
A custom laravel artisan command to run the development server using the app url set in the env file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* DevCommand.php | |
* | |
* A custom artisan command to run the development server using the app url set in the env file. | |
* | |
* This file contains the DevCommand class, which is a custom Laravel Artisan command used to simplify the process of | |
* running a development server. It provides functionalities to check server preconditions, start the server, handle | |
* permissions, and gracefully stop the server. This command is particularly useful during the development phase of a | |
* Laravel application. It starts both npm run dev and artisan serve simultaneously and provides output from both. | |
* | |
* Author: Midhun Monachan | |
* Version: 1.0 | |
* Date: 2024-05-13 | |
* Usage: This command is intended to be executed via the Laravel Artisan CLI tool. It can be used during local | |
* development to quickly start a development server. The command takes no arguments and can be invoked with: | |
* php artisan dev | |
*/ | |
namespace App\Console\Commands; | |
// Import necessary classes and dependencies | |
use Exception; | |
use Illuminate\Console\Command; | |
use Illuminate\Support\Facades\App; | |
use Symfony\Component\Process\Process; | |
use function Laravel\Prompts\confirm; | |
/** | |
* Class DevCommand | |
* | |
* A custom artisan command to run the development server. | |
*/ | |
class DevCommand extends Command | |
{ | |
/** | |
* The signature of the command. | |
* | |
* @var string | |
*/ | |
protected $signature = 'dev'; | |
/** | |
* The description of the command. | |
* | |
* @var string | |
*/ | |
protected $description = 'Run the development server'; | |
/** | |
* The host on which the server runs. | |
* | |
* @var string | |
*/ | |
protected $host = 'localhost'; | |
/** | |
* The port on which the server listens. | |
* | |
* @var string | |
*/ | |
protected $port = '80'; | |
/** | |
* Array to hold process instances. | |
* | |
* @var array | |
*/ | |
protected $processes = []; | |
/** | |
* Handle the command execution. | |
* | |
* @return void | |
*/ | |
public function handle() | |
{ | |
try { | |
// Check environment, setup signal handling, check server preconditions, and start server | |
$this->checkEnv()->setupSignalHandling()->checkServerPreconditions()->startServer(); | |
} catch (Exception $e) { | |
// Stop server in case of unexpected errors | |
$this->stopServer("Unexpected Error: " . $e->getMessage()); | |
exit; | |
} | |
} | |
/** | |
* Check server preconditions. | |
* | |
* @return $this | |
*/ | |
private function checkServerPreconditions() | |
{ | |
// Check permissions and prompt user if needed | |
if (!$this->checkPermissions($this->host, $this->port)) { | |
// Display warning message if permissions are insufficient | |
$this->displayMessage('You do not have necessary permissions to bind to ' . $this->host . ':' . $this->port, "WARN"); | |
// Ask for permission granting | |
if (confirm('Do you want to grant the necessary permissions? (This requires root access)')) { | |
$this->grantPermissions(); | |
} else { | |
// Abort if permission is denied | |
$this->displayMessage("Aborting. Not enough permission to bind to " . $this->host . ":" . $this->port, "ERROR"); | |
return; | |
} | |
} | |
// Check if server is already running | |
if ($this->isServerRunning($this->host, $this->port)) { | |
// Display warning message if server is already running | |
$this->displayMessage('A server is already running on port ' . $this->port . '.', "WARN"); | |
// Ask for killing the existing server | |
if (confirm('Do you wish to kill the existing server on port ' . $this->port . '?')) { | |
$this->killExistingServer($this->port); | |
} else { | |
return; | |
} | |
} | |
return $this; | |
} | |
/** | |
* Kill existing server running on specified port. | |
* | |
* @param string $port The port number of the server to be killed. | |
* @return void | |
*/ | |
private function killExistingServer($port) | |
{ | |
$command = "sudo lsof -t -i:$port | xargs kill -9"; | |
$process = Process::fromShellCommandline($command); | |
$process->run(); | |
if ($process->isSuccessful()) { | |
// Display success message if server is killed | |
$this->displayMessage('Existing server on port ' . $port . ' killed.', "SUCCESS"); | |
} else { | |
// Display error message if server couldn't be killed | |
$this->displayMessage('Aborting. Failed to kill the server.', "ERROR"); | |
exit; | |
} | |
} | |
/** | |
* Check if required permissions are granted. | |
* | |
* @param string $host The host to bind the server. | |
* @param string $port The port to listen on. | |
* @return bool True if permissions are granted, false otherwise. | |
*/ | |
private function checkPermissions($host, $port) | |
{ | |
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); | |
if ($socket === false) { | |
return false; | |
} | |
$bind = @socket_bind($socket, $host, $port); | |
socket_close($socket); | |
return $bind !== false; | |
} | |
/** | |
* Check if server is already running on specified host and port. | |
* | |
* @param string $host The host to check. | |
* @param string $port The port to check. | |
* @return bool True if server is running, false otherwise. | |
*/ | |
protected function isServerRunning($host, $port) | |
{ | |
// Try to open a connection to the localhost on the specified port | |
$connection = @fsockopen($host, $port); | |
// If the connection was opened successfully, a server is running on the port | |
if (is_resource($connection)) { | |
// Don't forget to close the connection! | |
fclose($connection); | |
return true; | |
} | |
// If the connection couldn't be opened, no server is running on the port | |
return false; | |
} | |
/** | |
* Grant necessary permissions for server. | |
* | |
* @return void | |
*/ | |
private function grantPermissions() | |
{ | |
$command = "sudo setcap 'cap_net_bind_service=+ep' `readlink -f $(which php)`"; | |
$process = Process::fromShellCommandline($command); | |
$process->run(); | |
if (!$process->isSuccessful()) { | |
// Display error message if permissions couldn't be granted | |
$this->displayMessage('Aborting. Error granting permissions.', "ERROR"); | |
exit; | |
} | |
// Display success message if permissions are granted | |
$this->displayMessage('Permissions granted.', "SUCCESS"); | |
} | |
/** | |
* Check environment for local execution. | |
* | |
* @return $this | |
*/ | |
private function checkEnv() | |
{ | |
if (App::environment() !== 'local') { | |
// Display error message if command is not executed in local environment | |
$this->displayMessage("This command can only be run in the local environment.", "ERROR"); | |
exit; | |
} | |
// Set host and port from environment variables | |
$this->setHostAndPortFromEnv()->validateHostAndPort(); | |
return $this; | |
} | |
/** | |
* Setup signal handling for graceful server shutdown. | |
* | |
* @return $this | |
*/ | |
private function setupSignalHandling() | |
{ | |
// Disable input and hide cursor | |
system('stty -echo'); | |
echo "\e[?25l"; | |
system("clear"); | |
// Handle SIGINT signal for graceful shutdown | |
pcntl_async_signals(true); | |
pcntl_signal(SIGINT, function () { | |
// Enable input and show cursor before stopping the server | |
system('stty echo'); | |
echo "\e[?25h"; | |
$this->stopServer("Stop signal received. Stopping the server...", "INFO"); | |
}); | |
return $this; | |
} | |
/** | |
* Set host and port from environment variables. | |
* | |
* @return $this | |
*/ | |
private function setHostAndPortFromEnv() | |
{ | |
// Get host and port from APP_URL environment variable | |
['host' => $this->host, 'port' => $this->port] = parse_url(env('APP_URL', 'http://localhost')) + ['port' => 80]; | |
return $this; | |
} | |
/** | |
* Validate host and port. | |
* | |
* @return void | |
*/ | |
private function validateHostAndPort() | |
{ | |
if (!filter_var(gethostbyname($this->host), FILTER_VALIDATE_IP)) { | |
// Display error message if hostname is invalid | |
$this->displayMessage("Invalid hostname: {$this->host}", "ERROR"); | |
exit; | |
} | |
if (!filter_var($this->port, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 65535]])) { | |
// Display error message if port is invalid | |
$this->displayMessage("Invalid port: {$this->port}", "ERROR"); | |
exit; | |
} | |
} | |
/** | |
* Start the development server. | |
* | |
* @return void | |
*/ | |
private function startServer() | |
{ | |
// Create and run npm and PHP processes | |
$this->processes[] = $this->createProcess(['npm', 'run', 'dev', '--host=' . $this->host]); | |
$this->processes[] = $this->createProcess(['php', 'artisan', 'serve', '--host=' . $this->host, '--port=' . $this->port]); | |
try { | |
$this->runProcesses(); | |
} catch (Exception $e) { | |
$this->stopServer("Unexpected Error: " . $e->getMessage()); | |
} | |
} | |
/** | |
* Create a new process instance. | |
* | |
* @param array $command The command to execute in the process. | |
* @return Process The process instance. | |
*/ | |
private function createProcess(array $command) | |
{ | |
$process = new Process($command); | |
$process->setTty(true); | |
return $process; | |
} | |
/** | |
* Run all processes. | |
* | |
* @return void | |
*/ | |
private function runProcesses() | |
{ | |
foreach ($this->processes as $process) { | |
$process->start(function ($type, $buffer) use ($process) { | |
if ($buffer && trim($buffer) !== '') { | |
$this->line(trim($buffer)); | |
} | |
}); | |
} | |
while (count($this->processes) > 0) { | |
foreach ($this->processes as $index => $process) { | |
if (!$process->isRunning()) { | |
unset($this->processes[$index]); | |
} | |
} | |
usleep(100000); // Sleep for 100 milliseconds | |
} | |
} | |
/** | |
* Stop the development server and display a message. | |
* | |
* @param string $message The message to display. | |
* @param string $type The type of message (SUCCESS, ERROR, WARN, INFO). | |
* @return void | |
*/ | |
private function stopServer($message = "", $type = "ERROR") | |
{ | |
foreach ($this->processes as $process) { | |
if ($process->isRunning()) { | |
$process->stop(); | |
} | |
} | |
foreach ($this->processes as $process) { | |
$process->wait(); | |
} | |
// Display the message with appropriate styling | |
$this->displayMessage($message, $type); | |
exit; | |
} | |
/** | |
* Display a message with colored output. | |
* | |
* @param string $message The message to display. | |
* @param string $type The type of message (SUCCESS, ERROR, WARN, INFO). | |
* @return void | |
*/ | |
private function displayMessage($message, $type = "INFO") | |
{ | |
switch ($type) { | |
case "SUCCESS": | |
$color = "green"; | |
break; | |
case "ERROR": | |
$color = "red"; | |
break; | |
case "WARN": | |
$color = "yellow"; | |
break; | |
default: | |
$color = "blue"; | |
break; | |
} | |
// Display message with colored background and foreground | |
$this->line(" <bg={$color};fg=white> {$type} </> <fg={$color}>{$message}</>"); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment