Skip to content

Instantly share code, notes, and snippets.

@midhunmonachan
Created May 13, 2024 14:53
Show Gist options
  • Save midhunmonachan/1b602059b3da42fdace118cac9452787 to your computer and use it in GitHub Desktop.
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.
<?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