PHAR en PHP: empaquetar una aplicación PHP en un único fichero ejecutable

Un PHAR (PHP Archive) es un fichero que empaqueta una aplicación PHP completa —código, recursos, dependencias— en un único fichero ejecutable, de forma similar a los JARs de Java o los ZIPs de Python (wheels). Composer utiliza internamente el formato PHAR para distribuir su propio ejecutable. También es útil para distribuir herramientas CLI, scripts de migración o aplicaciones sin necesidad de instalar dependencias en el servidor destino.

Habilitar la creación de PHARs

La creación de PHARs está desactivada por defecto en php.ini por seguridad:

; php.ini
phar.readonly = Off   ; necesario para crear PHARs
# O desactivar solo para el script de build:
php -d phar.readonly=Off crear_phar.php

Crear un PHAR desde un directorio

<?php
// crear_phar.php — ejecutar con: php -d phar.readonly=Off crear_phar.php

$pharFile = 'miherramienta.phar';

// Eliminar si ya existe
if (file_exists($pharFile)) {
    unlink($pharFile);
}

$phar = new Phar($pharFile);
$phar->startBuffering();

// Añadir todos los ficheros del directorio src/
$phar->buildFromDirectory(__DIR__ . '/src');

// Definir el stub (punto de entrada)
$stub = <<<STUB
#!/usr/bin/env php
<?php
Phar::mapPhar('miherramienta.phar');
require 'phar://miherramienta.phar/index.php';
__HALT_COMPILER();
STUB;

$phar->setStub($stub);
$phar->stopBuffering();

// Hacer el fichero ejecutable
chmod($pharFile, 0755);

echo "PHAR creado: $pharFile (" . round(filesize($pharFile) / 1024, 1) . " KB)n";

buildFromDirectory con filtro de ficheros

<?php
$phar = new Phar('miapp.phar');
$phar->startBuffering();

// Incluir solo ficheros .php y .json, excluir tests y directorios ocultos
$phar->buildFromDirectory(__DIR__ . '/src', '/.(php|json)$/');

// O con un iterador personalizado para más control
$dirIter = new RecursiveDirectoryIterator(
    __DIR__ . '/src',
    RecursiveDirectoryIterator::SKIP_DOTS
);
$iter = new RecursiveIteratorIterator($dirIter);

foreach ($iter as $fichero) {
    // Excluir tests y ficheros de configuración local
    if (str_contains($fichero->getPathname(), '/tests/')) continue;
    if ($fichero->getExtension() === 'log') continue;

    // Añadir fichero con ruta relativa dentro del phar
    $rutaRelativa = substr($fichero->getPathname(), strlen(__DIR__ . '/src/'));
    $phar->addFile($fichero->getPathname(), $rutaRelativa);
}

// Añadir fichero de string en memoria
$phar->addFromString('version.php', '<?php define("VERSION", "1.2.0");');

$phar->setStub($phar->createDefaultStub('index.php'));
$phar->stopBuffering();

Comprimir el PHAR

<?php
$phar = new Phar('miapp.phar');
$phar->startBuffering();
$phar->buildFromDirectory(__DIR__ . '/src');
$phar->setStub($phar->createDefaultStub('index.php'));
$phar->stopBuffering();

echo "Sin comprimir: " . round(filesize('miapp.phar') / 1024, 1) . " KBn";

// Comprimir con GZ (requiere ext-zlib)
$pharGz = $phar->compress(Phar::GZ, 'miapp.phar.gz');
echo "Con GZ: " . round(filesize('miapp.phar.gz') / 1024, 1) . " KBn";

// Comprimir con BZ2 (requiere ext-bz2)
$pharBz2 = $phar->compress(Phar::BZ2, 'miapp.phar.bz2');

// Comprimir ficheros individuales dentro del phar
$phar->startBuffering();
foreach ($phar as $fichero) {
    $fichero->compress(Phar::GZ);
}
$phar->stopBuffering();

Firmar un PHAR con clave privada

La firma garantiza que el PHAR no ha sido modificado. Importante para herramientas que se distribuyen como descarga:

<?php
// Generar par de claves (solo una vez)
$config = ['private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA];
$recurso = openssl_pkey_new($config);
openssl_pkey_export($recurso, $clavePrivada);
file_put_contents('firma.pem', $clavePrivada);

// Firmar el PHAR
$phar = new Phar('miapp.phar');
$phar->setSignatureAlgorithm(Phar::OPENSSL, file_get_contents('firma.pem'));

// Verificar la firma al usar el PHAR
try {
    $info = Phar::getSupportedSignatures();
    $firma = (new Phar('miapp.phar'))->getSignature();
    echo "Tipo de firma: " . $firma['hash_type'] . "n";
    echo "Hash: "          . $firma['hash'] . "n";
} catch (PharException $e) {
    echo "Firma inválida: " . $e->getMessage();
}

Ejemplo real: herramienta CLI en un PHAR

<?php
// src/index.php — punto de entrada del PHAR
#!/usr/bin/env php

// El autoloader funciona dentro del PHAR
spl_autoload_register(function(string $clase) {
    $ruta = 'phar://miherramienta.phar/' . str_replace('\', '/', $clase) . '.php';
    if (file_exists($ruta)) {
        require $ruta;
    }
});

// O si usas Composer, incluye el vendor del PHAR
// require 'phar://miherramienta.phar/vendor/autoload.php';

// Procesar argumentos de línea de comandos
$comando = $argv[1] ?? 'help';
$args    = array_slice($argv, 2);

$comandos = [
    'version' => fn() => echo "miherramienta v1.2.0n",
    'help'    => fn() => echo "Uso: miherramienta [version|procesar|help]n",
    'procesar' => function() use ($args) {
        $fichero = $args[0] ?? null;
        if (!$fichero || !file_exists($fichero)) {
            echo "Error: fichero no encontradon";
            exit(1);
        }
        // Procesar...
        echo "Procesado: $ficheron";
    },
];

if (isset($comandos[$comando])) {
    ($comandos[$comando])();
} else {
    echo "Comando desconocido: $comandon";
    exit(1);
}
<?php
// crear_phar.php
$phar = new Phar('miherramienta.phar');
$phar->startBuffering();
$phar->buildFromDirectory(__DIR__ . '/src');

// Stub con shebang para ejecutar directamente en Linux/macOS
$stub = "#!/usr/bin/env phpn" . Phar::createDefaultStub('index.php');
$phar->setStub($stub);
$phar->stopBuffering();
chmod('miherramienta.phar', 0755);
# Usar la herramienta
php miherramienta.phar version
# O directamente si tiene shebang y permisos de ejecución:
./miherramienta.phar procesar datos.csv

Leer contenido de un PHAR sin ejecutarlo

<?php
// Listar los ficheros de un PHAR
$phar = new Phar('miapp.phar');
foreach ($phar as $fichero) {
    echo $fichero->getPathname() . "n";
}

// Leer un fichero específico
$contenido = file_get_contents('phar://miapp.phar/config/app.json');
$config = json_decode($contenido, true);

// Extraer todos los ficheros
$phar->extractTo('/tmp/extraido/');

// Extraer ficheros específicos
$phar->extractTo('/tmp/', ['config/app.json', 'src/Core/Router.php']);

Consideraciones de seguridad

  • Deshabilita phar.readonly en producción solo si es necesario: los PHARs pueden contener código ejecutable malicioso. La directiva phar.readonly=On (por defecto) evita que scripts del servidor creen o modifiquen PHARs.
  • Deserialización en PHARs: en PHP < 8.0, acceder a metadatos de un PHAR (incluso con file_exists('phar://...')) podía desencadenar la deserialización de metadatos maliciosos. Esto se resolvió requiriendo que los PHARs estén firmados.
  • Firma obligatoria para distribución: si distribuis un PHAR, fírmalo siempre. Permite que los usuarios verifiquen que no ha sido alterado.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP