Content Index
Laravel offers the ability to execute commands directly in the operating system console or terminal through the Process class. This tool allows invoking external commands and programs, making it easy to integrate the Laravel ecosystem with other environments, such as Python.
This integration allows combining Laravel's structure with Python's specialized tools in areas such as data analysis, image processing, or working with Large Language Models (LLMs). In this way, Python can act as a service or API in charge of heavy processing, returning the results to Laravel for subsequent use.
1. The Python Script for Image Analysis
For this example, a Python script is used that analyzes an image and returns the metadata formatted as a dictionary (the equivalent of an associative array). The script returns a structure with the following data:
status: Processing status.path: File name and location.size: Image size in kilobytes.widthandheight: Dimensions in pixels.absolute_path: Absolute path of the file on the system.
app/Python/analyzer.py
"""Analyses a local file and returns its metadata in JSON format.
It only uses Python's standard library, so it does not require installing
Pillow, pytesseract, or any other external dependency.
Usage from Laravel:
Process::run(['python3', base_path('app/Python/analyzer.py'), $imagePath]);
"""
import json
import struct
import sys
from pathlib import Path
# JPEG "Start Of Frame" markers containing width and height.
JPEG_SOF_MARKERS = {
0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7,
0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF,
}
def _png_size(data: bytes):
if len(data) >= 24 and data[12:16] == b'IHDR':
width, height = struct.unpack('>II', data[16:24])
return width, height
return None
def _gif_size(data: bytes):
if len(data) >= 10:
width, height = struct.unpack('<HH', data[6:10])
return width, height
return None
def _bmp_size(data: bytes):
if len(data) >= 26:
width, height = struct.unpack('<ii', data[18:26])
return width, abs(height)
return None
def _jpeg_size(data: bytes):
index = 2 # Skips initial SOI marker (0xFFD8).
while index + 9 < len(data):
if data[index] != 0xFF:
index += 1
continue
marker = data[index + 1]
# Markers that do not carry block size.
if marker in (0xD8, 0xD9) or 0xD0 <= marker <= 0xD7:
index += 2
continue
block_size = struct.unpack('>H', data[index + 2:index + 4])[0]
if marker in JPEG_SOF_MARKERS:
height, width = struct.unpack('>HH', data[index + 5:index + 9])
return width, height
index += 2 + block_size
return None
def image_dimensions(file_path: Path):
"""Returns (width, height) reading only the file header."""
readers = {
'.png': _png_size,
'.gif': _gif_size,
'.bmp': _bmp_size,
'.jpg': _jpeg_size,
'.jpeg': _jpeg_size,
}
reader = readers.get(file_path.suffix.lower())
if reader is None:
return None
with file_path.open('rb') as handle:
header = handle.read(65536)
return reader(header)
def analyze_file(file_path: str) -> dict:
path = Path(file_path).expanduser()
if not path.exists():
return {'error': 'File does not exist: {}'.format(path)}
if not path.is_file():
return {'error': 'Path is not a file: {}'.format(path)}
try:
size = image_dimensions(path)
except OSError:
size = None
return {
'status': 'success',
'file_name': path.name,
'extension': path.suffix.lower(),
'size_bytes': path.stat().st_size,
'size_kb': round(path.stat().st_size / 1024, 2),
'width': size[0] if size else None,
'height': size[1] if size else None,
'absolute_path': str(path.resolve()),
}
def main(argv) -> int:
if len(argv) < 2:
print(json.dumps({'error': 'File path was not provided'}))
return 1
result = analyze_file(argv[1])
print(json.dumps(result))
return 0 if 'error' not in result else 1
if __name__ == '__main__':
sys.exit(main(sys.argv))2. Encapsulating Logic in an Action
To keep the business logic centralized, the call to the Python process is defined inside an Action class. The workflow structure includes the following stages:
- Executable Configuration: The base command is defined (for example,
python3or the specific path of the interpreter configured in the system). - Command Construction: The absolute path of the script (
analyzer.py) and the path of the image to be processed are passed as arguments. - Timeout Handling: An execution time limit is established to prevent the process from blocking the application if a prolonged delay occurs.
- Output Decoding: The script's output (in JSON format) is received, validated, and transformed into a array manageable by Laravel. If the output is invalid or the process fails, the corresponding exception is managed.
app/Actions/AnalyzeFileWithPython.php
<?php
namespace App\Actions;
use Illuminate\Support\Facades\Process;
use RuntimeException;
/**
* Executes the Python script app/Python/analyzer.py and returns its JSON output.
*/
class AnalyzeFileWithPython
{
/**
* Analyzes a local file passing its absolute path as an argument to Python.
*
* @return array<string, mixed>
*
* @throws RuntimeException If the process fails or does not return valid JSON.
*/
public function handle(string $absolutePath): array
{
$script = base_path('app/Python/analyzer.py');
// The argument array avoids the shell: no quotes need to be escaped nor
// are chained commands from the user request exposed.
// Uses the Process facade (introduced in Laravel 10) to run analyzer.py passing the file path ($absolutePath) as an argument.
$result = Process::path(dirname($script))
->timeout(30)
->run([
config('services.python.binary', 'python3'),
$script,
$absolutePath,
]);
$decoded = json_decode($result->output(), true);
if ($result->failed()) {
$message = $decoded['error'] ?? trim($result->errorOutput());
throw new RuntimeException($message !== '' ? $message : 'The Python script failed.');
}
if (! is_array($decoded)) {
throw new RuntimeException('The Python script did not return valid JSON.');
}
return $decoded;
}
}You can create a configuration to customize it, in this example, using the environment variable PYTHON_BINARY:
config/services.php
'python' => [
'binary' => env('PYTHON_BINARY', 'python3'),
],3. Implementation of Single Action Controller (Invokable)
To expose this functionality through an HTTP endpoint, a Single Action Controller is used via the magic method __invoke.
Unlike a conventional controller with multiple methods, an invokable controller allows defining a single responsibility per class. When registering the route, direct reference is made to the controller class without needing to specify a method in an array.
Inside the controller, the following is performed:
- The request is received and the necessary dependencies are injected.
- The existence and validity of the stored image (for example, in the
storage/app/publicdirectory) are verified. - The
handlemethod of the Action is invoked by passing the absolute path of the image. - The response is returned with the metadata obtained in JSON format.
routes/web.php
Route::get('/python/analyze', PythonAnalyzerController::class)->name('python.analyze');app/Http/Controllers/PythonAnalyzerController.php
<?php
namespace App\Http\Controllers;
use App\Actions\AnalyzeFileWithPython;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use RuntimeException;
use Symfony\Component\HttpFoundation\Response;
class PythonAnalyzerController extends Controller
{
/**
* Analyzes a file from storage/app/public using the Python script.
*
* GET /python/analyze?file=sample.jpeg
*/
public function __invoke(Request $request, AnalyzeFileWithPython $analyze): JsonResponse
{
$file = $request->query('file', 'sample.png');
// Only relative file names inside storage/app/public are allowed:
// any ../ would attempt to read outside the directory.
if (! is_string($file) || $file === '' || str_contains($file, '/') || str_contains($file, '\\')) {
return response()->json([
'laravel_status' => 'ERROR',
'message' => 'Provide a valid file name, for example: ?file=sample.png',
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
try {
$metadata = $analyze->handle(storage_path("app/public/{$file}"));
} catch (RuntimeException $exception) {
return response()->json([
'laravel_status' => 'ERROR',
'message' => $exception->getMessage(),
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
return response()->json([
'laravel_status' => 'OK',
'python_response' => $metadata,
]);
}
}4. Verification and Results
When executing the route with different test files (such as images in JPG or PNG format stored in the file system), the application runs the process in the background and returns a response with detailed image information:
python_response
status "success"
file_name "sample.png"
extension ".png"
size_bytes 1639
size_kb 1.6
width 640
height 400
absolute_path "/Users/andrescruz/Herd/larafirststep/storage/app/public/sample.png"Using the Process class in Laravel allows extending the capabilities of the application beyond the PHP ecosystem. As long as adequate permissions are available on the server or local environment, it is possible to run any command-line utility to delegate complex tasks to specialized external tools.