...

Building a Laravel Livewire Component to Manage Terminal Commands

In this blog post, we will walk through building a Laravel Livewire component that allows you to execute terminal commands from within your Laravel application. We'll leverage the Symfony Process component to handle the command execution. This component will also store the command history in the database for future reference.

Laravel: Ensure you have a Laravel project set up. If not, you can create one using the following command:

composer create-project --prefer-dist laravel/laravel blog
Livewire: Install Livewire in your Laravel project

composer require livewire/livewire
php artisan livewire:publish --config

Step 1: Setting Up the Environment

php artisan make:livewire Admin/Terminal/ManageTerminal
This command will generate two files: a PHP class file for the component logic and a Blade view file for the component's HTML.

Step 2: Creating the Command History Model and Migration

We need a model to store the command history. Create a model with a migration file using the following command:

php artisan make:model CommandHistory -m

public function up()
{
    Schema::create('command_histories', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->constrained()->onDelete('cascade');
        $table->text('command');
        $table->text('output')->nullable();
        $table->text('error')->nullable();
        $table->boolean('success');
        $table->timestamps();
    });
}

php artisan migrate

Step 3: Implementing the Livewire Component Logic

Open the generated Livewire component class (ManageTerminal.php) and add the following logic:

<?php

namespace App\Livewire\Admin\Terminal;

use App\Http\Traits\ImageUploadTrait;
use App\Models\CommandHistory;
use Livewire\Component;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use Livewire\Attributes\On;
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Livewire\Attributes\Url;

class ManageTerminal extends Component
{

    use ImageUploadTrait;

    public $command;
    public $output;
    public $success;
    public $error;
    public $timestamp;
    public $tip;

    #[Url]
    public $search = '';

    public function render()
    {
        $search = trim($this->search);
        $commandHistories = CommandHistory::where('command', 'like', '%' . $search . '%')->latest()->get();
        return view('livewire.admin.terminal.manage-terminal', ['commandHistories' => $commandHistories]);
    }

    public function executeCommand()
    {
        $this->validate([
            'command' => 'required'
        ]);
        try {
            // Get the command from the request
            Session::forget('command');
            $command = trim($this->command);
            Session::put('command', $command);
            // Update this path to your PHP installation path
            $phpPath = env('IMPORT_PHP_PATH') ?? 'D:\laravel9\php\php.exe';

            // Prepend the PHP path to the command
            if (strpos($command, 'php') === 0) {
                $command = str_replace('php', '"' . $phpPath . '"', $command);
            }

            // Define the working directory to the root of your Laravel project
            $workingDirectory = base_path();
            $env = array_merge($_ENV, [
                'APP_ENV' => env('APP_ENV'),
                'APP_KEY' => env('APP_KEY'),
                'DB_CONNECTION' => env('DB_CONNECTION'),
                'DB_HOST' => env('DB_HOST'),
                'DB_PORT' => env('DB_PORT'),
                'DB_DATABASE' => env('DB_DATABASE'),
                'DB_USERNAME' => env('DB_USERNAME'),
                'DB_PASSWORD' => env('DB_PASSWORD'),
            ]);

            // Execute the command using Symfony Process component
            $process = Process::fromShellCommandline($command, $workingDirectory );
            // $process = new Process([$command], $workingDirectory, $env);
            $process->setInput("Yes\n");
            // Run the command
            $process->run();

            // Check if the process was successful
            if (!$process->isSuccessful()) {
                throw new ProcessFailedException($process);
            }
            $this->output = $process->getOutput();
            $this->success = $process->isSuccessful();
            // $this->success = true;
            $this->timestamp = now()->format('Y-m-d H:i:s');
            $this->tip = 'For more commands, visit the documentation or contact support if you encounter any issues.';
            // Get the output of the command
            $commandHistory = new CommandHistory();
            $commandHistory->user_id = auth()->id();
            $commandHistory->command = $command;
            $commandHistory->output = $this->success ? $this->output : null;
            $commandHistory->error = $this->success ? null : $process->getErrorOutput();
            $commandHistory->success = $this->success;
            $commandHistory->save();
            $this->reset(['command']);

            $this->myAlert([
                'message' => 'Command executed successfully',
                'alert-type' => 'success'
            ]);
        } catch (\Exception $e) {
            // Handle exceptions (e.g., ProcessFailedException)
            Log::error('Error executing command: ' . $e->getMessage());
            $this->error = $e->getMessage();
            $this->success = false;
            $this->output = null;
            $this->timestamp = now()->format('Y-m-d H:i:s');
            $this->tip = 'For more commands, visit the documentation or contact support if you encounter any issues.';

            $this->myAlert([
                'message' => 'An error occurred',
                'alert-type' => 'error'
            ]);
        }
    }

    public function delete($rowId)
    {

        $commad =  CommandHistory::findOrFail($rowId);
        $commad->delete();

        $this->myAlert([
            'message' => 'Deleted successfully',
            'alert-type' => 'error'
        ]);
    }



    public function copyCommand($id)
    {
        $commandHistory = CommandHistory::findOrFail($id);
        $phpPath = env('IMPORT_PHP_PATH');
        $command = str_replace($phpPath, 'php', $commandHistory->command);
        $command = str_replace('"php"', 'php', $command);
        $this->dispatch('copySelectedCommand', ['commandName' =>  trim($command)]);
    }




    #[On('copiedSuccess')]
    public function showAlert($copiedText)
    {
        try {
            // Replace 'php artisan' with empty string
            $copiedCmd = str_replace('php artisan', '', $copiedText);

            // Show success alert
            $this->myAlert([
                'message' =>  $copiedCmd . ' copied successfully.',
                'alert-type' => 'success'
            ]);
        } catch (\Exception $e) {
            // Handle any exceptions here
            $this->myAlert([
                'message' => 'An error occurred while processing the copied text.',
                'alert-type' => 'error'
            ]);
        }
    }
    public $commandRowId = [];

    public function checkAll()
    {
        $getPageIds =    CommandHistory::get();
        if ($getPageIds) {
            $this->commandRowId =  $getPageIds->pluck('id')->toArray();
        } else {
            $this->commandRowId = [];
        }
    }

    public function deleteAll()
    {
        if (count($this->commandRowId) > 0) {
            CommandHistory::whereIn('id', $this->commandRowId)->delete();
            $this->myAlert([
                'message' => 'Commands Deleted Deleted',
                'alert-type' => 'error'
            ]);

            $this->commandRowId = [];
        } else {
            $this->myAlert([
                'message' => 'Please Select First',
                'alert-type' => 'error'
            ]);
        }
    }
}

Step 4: Create Blade View Create a Blade view to render the Livewire component. You can customize the UI as needed. Here's a basic example:

<div>
    <div class="row" >
        <div class="col-md-12">
          <div class="card card-outline card-info ">
            <div class="card-header">
              <h3 class="card-title">Run Command</h3>
            </div>
            <div class="card-body">
                <form wire:submit.prevent="executeCommand" > 
                        <div class="row">
                            <div class="col-md-12">
                                <div class="mb-3">
                                    <input type="text"  wire:model="command" class="form-control @error('command') is-invalid   @enderror" autocomplete="off"
                                          placeholder="Enter command...">
                                    @error('command') <span class="text-danger">{{$message}}</span> @enderror
                                </div>
                            </div>
                        </div>
                        <div>
                            <button type="submit" class="btn btn-success" wire:loading.attr="disabled" wire:target="executeCommand">Run</button>
                            <i class="fas fa-1x fa-sync-alt fa-spin"   wire:loading  wire:target="executeCommand" ></i>
                        </div>
                </form>

                @if($success)
                    <div class="mt-3">
                        <h3 class="text-success">???? Command executed successfully!</h3>
                        <pre class="bg-dark">{{ $output }}</pre>
                        <p class="text-success font-weight-bold">{{ $timestamp ?? '' }}</p>
                        <p class="text-info font-weight-bold"> {{ $tip ?? '' }}</p>
                    </div>
                @elseif($error)
                    <div class="mt-3">
                        <h3 class="text-danger">⚠️ An error occurred while executing the command.</h3>
                        <pre class="bg-dark">{!! $error !!}</pre>
                        <p class="text-success font-weight-bold">{{ $timestamp ?? '' }}</p>
                        <p class="text-info font-weight-bold"> {{ $tip ?? '' }}</p>
                    </div>
                @endif

                @if (session()->has('command'))
                    <div class="alert alert-dark mt-3">
                        <strong>Recently used: </strong> 
                        <span id="usedCommad">{{ session('command') }}</span>
                        <button id="copyButton" class="btn btn-primary ml-3">Copy</button>
                    </div>
                @endif
   
            </div>
          </div>
        </div>
      </div>
      @if(isset($commandHistories) && count($commandHistories) > 0)
        <div class="row">
            <div class="col-md-12">
                <div class="card card-outline card-info ">
                    <div class="card-header">
                        <h3 class="card-title">Run Terminal Commands</h3>
                        <div class="card-tools">
                            <button type="button" class="btn btn-tool" data-card-widget="collapse" title="Collapse">
                                <i class="fas fa-minus"></i>
                            </button>
                        </div>
                    </div>
                    <div class="card-body table-responsive p-0">
                        <div class="card-header ">
                            @hasanyrole('Super Admin|admin')
                            <button type="button" class="btn btn-danger btn-sm" wire:click="deleteAll" wire:loading.attr="disabled"
                            wire:target="deleteAll">
                            Delete all
                        </button>
                        <button type="button" class="btn btn-info btn-sm" wire:click="checkAll" wire:loading.attr="disabled"
                            wire:target="checkAll">
                            Check all
                        </button>
                            @else
                            I do not have all of these roles or have more other roles...
                            @endhasanyrole
                            <i class="fas fa-1x fa-sync-alt fa-spin" wire:loading wire:target="openCreateModel"></i>
                            <div class="card-tools">
                                <div class="input-group input-group-sm" style="width: 150px;">
                                    <input type="text" name="table_search" class="form-control float-right"
                                        wire:model.live="search" placeholder="Search">
                                    <div class="input-group-append">
                                        <button type="submit" class="btn btn-default">
                                            <i class="fas fa-search"></i>
                                        </button>
                                    </div>
                                </div>
                            </div>
                        </div>
                        <table class="table table-hover text-nowrap">
                            <thead>
                                <tr>
                                    <th scope="col">User</th>
                                    <th scope="col">Command</th>
                                    <th scope="col">Time</th>
                                    <th scope="col">Status</th>
                                    <th scope="col">Action</th>
                                </tr>
                            </thead>
                            <tbody>
                                @foreach ($commandHistories as $history)
                                <tr>
                                    <th scope="row"> <input type="checkbox" wire:model="commandRowId"
                                        value="{{$history->id}}">   {{ $history->user->name ?? 'Unknown' }}</th>
                                    <td>
                                    @php
                                    $phpPath = env('IMPORT_PHP_PATH', 'D:\laravel9\php\php.exe');
                                    $command = str_replace("$phpPath",  'php',  $history->command);
                                    $command = str_replace('"php"', 'php', $command);
                                    @endphp
                                    <a href="javascript:void(0)" title="{{$history->command}}">  {{  $command  ?? '' }} </a>
                                    </td>
                                    <td>{{ $history->created_at->diffForHumans() }}</td>
                                    <td>
                                        @if ($history->success)
                                            <span class="badge badge-success">Success</span>
                                        @else
                                            <span class="badge badge-danger">Failed</span>
                                        @endif
                                    </td>
                                    <td> 
                                        <button  class="btn btn-success " wire:target="copyCommand({{$history->id}})" wire:loading.attr="disabled" 
                                          wire:click.prevent="copyCommand({{$history->id}})">
                                            <i class="fas fa-copy" wire:loading.remove wire:target="copyCommand({{ $history->id }})"></i>
                                            <i class="fas fa-spinner fa-spin" wire:loading wire:target="copyCommand({{ $history->id }})"></i>
                                        </button>
                                        <button  class="btn btn-danger " wire:target="delete({{$history->id}})" wire:loading.attr="disabled" 
                                              wire:click.prevent="delete({{$history->id}})">
                                            <i class="fas fa-trash" wire:loading.remove wire:target="delete({{ $history->id }})"></i>
                                            <i class="fas fa-spinner fa-spin" wire:loading wire:target="delete({{ $history->id }})"></i>
                                        </button>
                                    </td>
                                </tr>
                            @endforeach
                            </tbody>
                        </table>
                    </div>
                </div>
            </div>
        </div>
      @endif

    <script>
        document.getElementById('copyButton').addEventListener('click', function() {
            var textToCopy = document.getElementById('usedCommad').innerText;
            var tempTextarea = document.createElement('textarea');
            tempTextarea.value = textToCopy;
            document.body.appendChild(tempTextarea);
            tempTextarea.select();
            tempTextarea.setSelectionRange(0, 99999); // For mobile devices
            document.execCommand('copy');
            document.body.removeChild(tempTextarea);
            var usedCommadSpan = document.getElementById('usedCommad');
            usedCommadSpan.classList.add('bg-danger', 'p-2');
            setTimeout(function() {
                copyButton.textContent = 'Copy';
                usedCommadSpan.classList.remove('bg-danger', 'p-3'); // Remove the classes
            }, 2000);
            Livewire.dispatch('copiedSuccess', { copiedText: textToCopy}) ;


            
        });

        document.addEventListener('livewire:init', () => {
            Livewire.on('copySelectedCommand', (event) => {
                let copiedCommand  = event[0]['commandName'];
                    // console.log(copiedCommand);
                    var tempTextarea = document.createElement('textarea');
                    tempTextarea.value = copiedCommand;
                    document.body.appendChild(tempTextarea);
                    tempTextarea.select();
                    tempTextarea.setSelectionRange(0, 99999); // For mobile devices
                    document.execCommand('copy');
                    document.body.removeChild(tempTextarea);
                    Livewire.dispatch('copiedSuccess', { copiedText: copiedCommand}) ;
            });
        });
    </script>
</div>

William Anderson

I am a versatile Full-Stack Web Developer with a strong focus on Laravel, Livewire, Vue.js, and Tailwind CSS. With extensive experience in backend development, I specialize in building scalable, efficient, and high-performance web applications.