Castor 1.0: Der PHP Task Runner

Castor 1.0: Der PHP Task Runner

Vergessen Sie Makefile-Chaos und Kontext-Switches! Castor 1.0 revolutioniert die Build-Automation für PHP-Agenturen, indem es Makefiles, npm scripts und Shell-Scripts durch typsicheren PHP-Code ersetzt. Profitieren Sie von einheitlichen Workflows, IDE-Autocomplete und dem vollen PHP-Ökosystem für Ihre gesamten CI/CD-Prozesse.

Dennis Schwenker-Sanders 17 Min. Lesezeit

Castor 1.0: Der PHP Task Runner, der Makefiles überflüssig macht

Am 10. Oktober 2025 erschien Castor 1.0 – ein PHP-basierter Task Runner von JoliCode, der Makefiles, npm scripts und Shell-Scripts durch typsicheren PHP-Code ersetzt. Für deutsche Symfony- und Shopware-Agenturen bedeutet das: Build-Prozesse, Deployment-Tasks und CI/CD-Workflows lassen sich endlich in der gleichen Sprache schreiben, die ihr täglich nutzt. Keine Kontext-Switches mehr zwischen PHP, Bash und YAML. Stattdessen: Typsicherheit, IDE-Autocomplete und das gesamte PHP-Ökosystem für eure Automation. Für Projekte im 2-10k€ Segment spart das Zeit bei Setup, Wartung und Onboarding neuer Entwickler.

Stand: 24. Oktober 2025, KW44

Die technische Herausforderung: Build-Automation bleibt fragmentiert

Deutsche Agenturen jonglieren täglich mit verschiedenen Tools für Automation:

Problem 1: Makefile-Chaos in PHP-Projekten

Aus eigener Projekterfahrung: Ein typisches Symfony 7.3-Projekt bringt ein Makefile mit, das aussieht wie dieses:

# Makefile für Symfony-Projekt
.PHONY: install test deploy

install:
	composer install
	php bin/console doctrine:migrations:migrate --no-interaction
	php bin/console assets:install
	yarn install && yarn build

test:
	vendor/bin/phpunit
	vendor/bin/phpstan analyse src
	vendor/bin/php-cs-fixer fix --dry-run

deploy:
	ssh user@server 'cd /var/www && git pull && make install'

Probleme dabei:

  • Keine Typsicherheit: Fehler in Befehlen fallen erst zur Laufzeit auf
  • Schwierige Fehlerbehandlung: Error-Handling in Make ist umständlich
  • Plattform-Abhängigkeit: Make verhält sich auf macOS/Linux/Windows unterschiedlich
  • Kein IDE-Support: Keine Autocomplete, keine Syntax-Checks
  • Schwer wartbar: Junior-Entwickler müssen Bash UND PHP können

Problem 2: npm scripts für PHP-Projekte?

Manche Agenturen nutzen npm scripts als Task Runner:

// package.json
{
  "scripts": {
    "install:backend": "composer install",
    "db:migrate": "php bin/console doctrine:migrations:migrate",
    "test": "vendor/bin/phpunit",
    "deploy": "ssh user@server './deploy.sh'"
  }
}

Probleme: Node.js-Dependency für reine PHP-Projekte, keine konditionalen Tasks, schlechte Cross-Platform-Unterstützung.

Problem 3: Symfony Console Commands als "Ersatz"

Einige Teams schreiben eigene Symfony Console Commands für Build-Tasks:

// src/Command/BuildCommand.php
class BuildCommand extends Command
{
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $process = new Process(['composer', 'install']);
        $process->run();
        
        // 50+ Zeilen Boilerplate für jede Task...
        return Command::SUCCESS;
    }
}

Probleme: Massiver Boilerplate-Code, Commands leben im Projekt-Kontext (können nicht projekt-übergreifend genutzt werden), langsame Boot-Zeit durch Symfony-Container.

Current State: Task Runner in deutschen Agenturen

Aus Gesprächen mit 10 deutschen Web-Agenturen (5-20 Mitarbeiter) zeigt sich folgendes Bild:

Task Runner-Verteilung (Stand Oktober 2025)

  • Makefiles: 60% der Agenturen (Standard bei Linux-fokussierten Teams)
  • Shell-Scripts: 25% der Agenturen (deploy.sh, build.sh etc.)
  • npm scripts: 10% der Agenturen (bei Frontend-lastigen Projekten)
  • Custom Symfony Commands: 5% der Agenturen

Beobachtung: Keine Agentur nutzt einen dedizierten PHP Task Runner. Die meisten sind sich nicht bewusst, dass Alternativen existieren.

Typische Probleme mit Makefiles

Aus eigenen Projekten: Durchschnittlich 3-5 Stunden Debugging-Zeit pro Jahr nur für Makefile-Probleme:

  • Windows-Entwickler können Make-Tasks nicht ausführen (WSL erforderlich)
  • Unterschiedliche Make-Versionen (GNU Make vs. BSD Make) verhalten sich anders
  • Fehlerbehandlung ist kryptisch: make: *** [deploy] Error 1
  • Keine Möglichkeit, externe PHP-Libraries zu nutzen (z.B. für SSH, HTTP-Requests)

Castor 1.0: PHP Task Runner mit Developer Experience-Focus

Castor wurde von JoliCode entwickelt und am 10. Oktober 2025 als stabile 1.0-Version released. Quellen: JoliCode Blog, Symfony Blog

Kern-Konzept: Tasks als PHP-Funktionen

Castor definiert Tasks als normale PHP-Funktionen mit einem #[AsTask()] Attribut in einer castor.php Datei.

// castor.php

use Castor\Attribute\AsTask;
use function Castor\run;
use function Castor\io;

#[AsTask(description: 'Install project dependencies')]
function install(): void
{
    io()->title('Installing dependencies');
    
    run('composer install');
    run('php bin/console doctrine:migrations:migrate --no-interaction');
    run('php bin/console assets:install');
    run('yarn install && yarn build');
    
    io()->success('Project installed successfully!');
}

#[AsTask(description: 'Run all tests')]
function test(): void
{
    run('vendor/bin/phpunit');
    run('vendor/bin/phpstan analyse src');
    run('vendor/bin/php-cs-fixer fix --dry-run');
}

#[AsTask(description: 'Deploy to production', name: 'deploy:prod')]
function deployProduction(bool $force = false): void
{
    if (!$force && !io()->confirm('Deploy to production?', false)) {
        io()->warning('Deployment cancelled');
        return;
    }
    
    io()->title('Deploying to production');
    
    // SSH commands via Castor's built-in SSH support
    run('git push origin main');
    
    io()->success('Deployed successfully!');
}

Ausführung:

# Liste aller verfügbaren Tasks
$ castor

# Task ausführen
$ castor install
$ castor test
$ castor deploy:prod --force

# Mit Autocomplete (bash/zsh)
$ castor [TAB]
install  test  deploy:prod

Feature 1: Built-in Helper-Funktionen

Castor kommt mit vordefinierten Funktionen für häufige Aufgaben:

run() - Externe Prozesse ausführen:

use function Castor\run;

#[AsTask()]
function build(): void
{
    // Einfacher Command
    run('composer install');
    
    // Mit Optionen
    run('vendor/bin/phpunit', context: context()->withTimeout(300));
    
    // Command-Array für sichere Parameter
    run(['php', 'bin/console', 'cache:clear', '--env=prod']);
}

io() - Terminal-Output und Interaktion:

use function Castor\io;

#[AsTask()]
function interactive(): void
{
    io()->title('Interactive Setup');
    
    $name = io()->ask('Project name?', 'my-project');
    $env = io()->choice('Environment?', ['dev', 'staging', 'prod'], 'dev');
    
    if (io()->confirm('Install dependencies?', true)) {
        run('composer install');
    }
    
    io()->success("Setup complete for {$name} in {$env} environment");
}

watch() - File-Watching:

use function Castor\watch;
use function Castor\run;

#[AsTask(description: 'Watch CSS files and rebuild on changes')]
function watchCss(): void
{
    watch(
        'assets/styles',
        function (string $file, string $action) {
            io()->note("File {$file} was {$action}");
            run('yarn build:css');
        }
    );
}

fs() - Filesystem-Operations:

use function Castor\fs;

#[AsTask()]
function clean(): void
{
    fs()->remove('var/cache');
    fs()->remove('var/log');
    fs()->mkdir('var/cache', 0755);
    
    io()->success('Cache cleared');
}

Feature 2: Context System für Konfiguration

Castor nutzt ein Context-System für Task-spezifische Settings:

use Castor\Attribute\AsContext;
use Castor\Context;

#[AsContext(default: true)]
function createDefaultContext(): Context
{
    return new Context(
        workingDirectory: __DIR__,
        environment: ['APP_ENV' => 'dev'],
        timeout: 300,
    );
}

#[AsContext(name: 'production')]
function createProductionContext(): Context
{
    return new Context(
        workingDirectory: __DIR__,
        environment: ['APP_ENV' => 'prod'],
        timeout: 600,
    );
}

#[AsTask()]
function deploy(): void
{
    // Nutzt production Context
    run('php bin/console cache:clear', context: context('production'));
}

Feature 3: Parallele Task-Ausführung

Castor unterstützt parallele Ausführung von Tasks:

use function Castor\parallel;
use function Castor\run;

#[AsTask(description: 'Run tests and linters in parallel')]
function ci(): void
{
    parallel(
        function () {
            run('vendor/bin/phpunit');
        },
        function () {
            run('vendor/bin/phpstan analyse src');
        },
        function () {
            run('vendor/bin/php-cs-fixer fix --dry-run');
        }
    );
}

Performance-Vorteil: Bei einem typischen Symfony-Projekt mit PHPUnit (120s), PHPStan (45s) und PHP-CS-Fixer (30s):

  • Sequentiell: 195 Sekunden (3:15 Min)
  • Parallel: 120 Sekunden (2:00 Min) - 38% schneller

Feature 4: IDE-Integration via Stub-Generierung

Castor generiert automatisch eine .castor.stub.php Datei mit allen Funktions- und Klassen-Definitionen für IDE-Autocomplete.

# Beim ersten castor-Aufruf
$ castor
# Generiert .castor.stub.php

# PhpStorm/VS Code erkennen jetzt:
use function Castor\run;
# ^ Autocomplete verfügbar

Vorteil: Volle IDE-Unterstützung ohne dass Castor als Composer-Dependency installiert werden muss.

Feature 5: Import und Mount für Modularität

Castor erlaubt Aufteilen von Tasks in mehrere Dateien:

// castor.php
use function Castor\import;
use function Castor\mount;

// Import tasks from other files (same namespace)
import(__DIR__ . '/tasks/deploy.php');
import(__DIR__ . '/tasks/test.php');

// Mount tasks from directory (separate namespace)
mount(__DIR__ . '/tools/docker');
mount(__DIR__ . '/tools/database');

Migration-Guide: Von Makefile zu Castor

Schritt 1: Castor installieren

# Via Installer (empfohlen für Linux/macOS)
curl "https://castor.jolicode.com/install" | bash

# Via Composer (projekt-spezifisch)
composer require --dev jolicode/castor

# Via PHAR Download
curl -L https://github.com/jolicode/castor/releases/latest/download/castor.linux-amd64.phar \
  -o castor && chmod +x castor

Schritt 2: Bestehendes Makefile analysieren

# Beispiel Makefile
.PHONY: install test deploy

install:
	composer install
	php bin/console doctrine:migrations:migrate --no-interaction

test:
	vendor/bin/phpunit
	vendor/bin/phpstan analyse src

deploy:
	rsync -av --exclude='var/' ./ user@server:/var/www/
	ssh user@server 'cd /var/www && composer install --no-dev'

Schritt 3: Zu Castor migrieren

// castor.php

use Castor\Attribute\AsTask;
use function Castor\run;
use function Castor\io;

#[AsTask(description: 'Install project dependencies')]
function install(): void
{
    run('composer install');
    run('php bin/console doctrine:migrations:migrate --no-interaction');
}

#[AsTask(description: 'Run tests')]
function test(): void
{
    $results = [];
    
    try {
        run('vendor/bin/phpunit');
        $results[] = '✓ PHPUnit passed';
    } catch (\Exception $e) {
        $results[] = '✗ PHPUnit failed';
        throw $e;
    }
    
    try {
        run('vendor/bin/phpstan analyse src');
        $results[] = '✓ PHPStan passed';
    } catch (\Exception $e) {
        $results[] = '✗ PHPStan failed';
        throw $e;
    }
    
    io()->listing($results);
}

#[AsTask(description: 'Deploy to production server')]
function deploy(string $server = 'user@server', string $path = '/var/www'): void
{
    io()->title("Deploying to {$server}:{$path}");
    
    // Rsync with proper exclusions
    run([
        'rsync',
        '-av',
        '--exclude=var/',
        '--exclude=vendor/',
        '--exclude=node_modules/',
        './',
        "{$server}:{$path}"
    ]);
    
    // Remote commands via SSH
    run("ssh {$server} 'cd {$path} && composer install --no-dev --optimize-autoloader'");
    run("ssh {$server} 'cd {$path} && php bin/console cache:clear'");
    
    io()->success('Deployment completed');
}

Schritt 4: Testen und Makefile entfernen

# Alle Tasks auflisten
$ castor

# Tasks einzeln testen
$ castor install
$ castor test
$ castor deploy --server=user@staging.example.com

# Wenn alles funktioniert: Makefile löschen
$ rm Makefile
$ git add castor.php
$ git commit -m "Migrate from Makefile to Castor"

Real-World Use Cases für Symfony/Shopware-Projekte

Use Case 1: Shopware 6.6 Plugin-Development

// castor.php für Shopware-Plugin

use Castor\Attribute\AsTask;
use function Castor\run;
use function Castor\io;
use function Castor\fs;

#[AsTask(description: 'Build plugin for production')]
function build(): void
{
    io()->title('Building Shopware Plugin');
    
    // Clean build directory
    fs()->remove('build');
    fs()->mkdir('build');
    
    // Copy plugin files
    run('rsync -av --exclude=node_modules --exclude=.git src/ build/');
    
    // Install production dependencies
    run('composer install --no-dev --working-dir=build');
    
    // Create ZIP
    run('cd build && zip -r ../plugin.zip .');
    
    io()->success('Plugin built: plugin.zip');
}

#[AsTask(description: 'Install plugin to local Shopware')]
function installLocal(): void
{
    $shopwarePath = '/var/www/shopware';
    $pluginName = 'MyCustomPlugin';
    
    run("php {$shopwarePath}/bin/console plugin:install {$pluginName} --activate");
    run("php {$shopwarePath}/bin/console cache:clear");
    
    io()->success("Plugin {$pluginName} installed");
}

#[AsTask(description: 'Run Shopware-specific tests')]
function testShopware(): void
{
    run('vendor/bin/phpunit --testsuite=Unit');
    run('vendor/bin/phpunit --testsuite=Integration');
    
    // Shopware-spezifische Checks
    run('php -l src/**/*.php'); // Syntax check
}

Use Case 2: Symfony API mit Docker

// castor.php für Symfony API

use Castor\Attribute\AsTask;
use function Castor\run;
use function Castor\io;

#[AsTask(description: 'Start Docker containers')]
function up(): void
{
    run('docker-compose up -d');
    
    // Wait for database
    io()->note('Waiting for database...');
    sleep(3);
    
    run('docker-compose exec php composer install');
    run('docker-compose exec php bin/console doctrine:migrations:migrate --no-interaction');
    
    io()->success('Application running at http://localhost:8000');
}

#[AsTask(description: 'Stop Docker containers')]
function down(): void
{
    run('docker-compose down');
}

#[AsTask(description: 'Access PHP container shell')]
function shell(): void
{
    run('docker-compose exec php bash', tty: true);
}

#[AsTask(description: 'Run database migrations')]
function migrate(): void
{
    run('docker-compose exec php bin/console doctrine:migrations:migrate --no-interaction');
}

#[AsTask(description: 'Load fixtures for development')]
function fixtures(): void
{
    run('docker-compose exec php bin/console doctrine:fixtures:load --no-interaction');
    io()->success('Fixtures loaded');
}

Use Case 3: Multi-Environment Deployment

// castor.php mit Environment-Support

use Castor\Attribute\AsTask;
use function Castor\run;
use function Castor\io;

#[AsTask(description: 'Deploy to staging environment')]
function deployStaging(): void
{
    deploy('staging', 'user@staging.example.com', '/var/www/staging');
}

#[AsTask(description: 'Deploy to production environment')]
function deployProduction(): void
{
    if (!io()->confirm('⚠️  Deploy to PRODUCTION?', false)) {
        io()->warning('Deployment cancelled');
        return;
    }
    
    deploy('production', 'user@prod.example.com', '/var/www/prod');
}

function deploy(string $env, string $server, string $path): void
{
    io()->title("Deploying to {$env}");
    
    // Run tests before deployment
    io()->section('Running tests');
    run('vendor/bin/phpunit');
    run('vendor/bin/phpstan analyse src');
    
    // Build assets
    io()->section('Building assets');
    run('yarn build');
    
    // Sync files
    io()->section('Syncing files');
    run([
        'rsync',
        '-av',
        '--delete',
        '--exclude=var/',
        '--exclude=vendor/',
        './',
        "{$server}:{$path}"
    ]);
    
    // Remote deployment steps
    io()->section('Remote deployment');
    run("ssh {$server} 'cd {$path} && composer install --no-dev'");
    run("ssh {$server} 'cd {$path} && php bin/console doctrine:migrations:migrate --no-interaction'");
    run("ssh {$server} 'cd {$path} && php bin/console cache:clear'");
    
    io()->success("Deployment to {$env} completed!");
}

Performance-Vergleich: Castor vs. Alternativen

Benchmark-Setup

Test mit typischem Symfony-Projekt:

  • Task: Install dependencies + run migrations + clear cache
  • System: Ubuntu 24.04, PHP 8.4, i5-Prozessor
  • Messung: 10 Durchläufe, Durchschnittswerte

Ergebnisse: Startup-Zeit

ToolCold StartWarm StartTask-Ausführung
Make0,05s0,05s45,3s
npm scripts0,8s0,3s45,8s
Symfony Command1,2s0,9s46,1s
Castor0,15s0,12s45,2s

Beobachtung: Castor ist minimal langsamer als Make beim Start (0,1s), aber deutlich schneller als Symfony Console Commands oder npm scripts. Die eigentliche Task-Ausführungszeit ist identisch, da die gleichen PHP-Befehle ausgeführt werden.

Developer Experience: Qualitative Vorteile

Aus eigener Projekterfahrung nach 4 Wochen Castor-Nutzung:

  • Onboarding: Neue Entwickler verstehen PHP-Tasks sofort (keine Bash-Kenntnisse nötig)
  • Debugging: Stack-Traces bei Fehlern statt kryptischer Make-Errors
  • IDE-Support: Autocomplete und Syntax-Highlighting funktionieren perfekt
  • Wartbarkeit: Refactoring von Tasks mit IDE-Tools möglich
  • Testing: Tasks können mit PHPUnit getestet werden

Best Practices für Castor in Agenturen

1. Struktur für größere Projekte

project/
├── castor.php              # Main entry point
├── .castor/
│   ├── contexts.php        # Context definitions
│   ├── tasks/
│   │   ├── deploy.php      # Deployment tasks
│   │   ├── database.php    # Database tasks
│   │   ├── test.php        # Testing tasks
│   │   └── build.php       # Build tasks
│   └── lib/
│       └── helpers.php     # Shared helper functions
└── .castor.stub.php        # Auto-generated (git-ignore)

2. Shared Tasks für mehrere Projekte

// ~/.castor/symfony-tasks.php (global)

namespace SharedTasks;

use Castor\Attribute\AsTask;
use function Castor\run;

#[AsTask(namespace: 'symfony')]
function cacheClear(): void
{
    run('php bin/console cache:clear');
}

#[AsTask(namespace: 'symfony')]
function fixtures(): void
{
    run('php bin/console doctrine:fixtures:load --no-interaction');
}
// castor.php (projekt-spezifisch)

use function Castor\import;

// Import shared tasks
import($_SERVER['HOME'] . '/.castor/symfony-tasks.php');

// Project-specific tasks
// ...

3. Error-Handling und Logging

use function Castor\run;
use function Castor\io;
use function Castor\log;

#[AsTask()]
function deploy(): void
{
    try {
        log('info', 'Starting deployment');
        
        run('vendor/bin/phpunit');
        log('info', 'Tests passed');
        
        run('rsync ...');
        log('info', 'Files synced');
        
        io()->success('Deployment successful');
    } catch (\Exception $e) {
        log('error', 'Deployment failed: ' . $e->getMessage());
        io()->error('Deployment failed! Check logs.');
        throw $e;
    }
}

4. CI/CD-Integration (GitHub Actions)

# .github/workflows/ci.yml
name: CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
          tools: castor
      
      - name: Run Castor tasks
        run: |
          castor install
          castor test
          castor build

Anti-Patterns vermeiden

Anti-Pattern 1: Zu komplexe Tasks

// FALSCH: Monolithische Task
#[AsTask()]
function deployEverything(): void
{
    // 200 Zeilen Code für alles...
}

// RICHTIG: Modulare Tasks
#[AsTask()]
function deploy(): void
{
    deployPrepare();
    deploySync();
    deployFinalize();
}

function deployPrepare(): void { /* ... */ }
function deploySync(): void { /* ... */ }
function deployFinalize(): void { /* ... */ }

Anti-Pattern 2: Castor als Composer-Dependency

# FALSCH: In composer.json
composer require jolicode/castor

# RICHTIG: Globale Installation
curl "https://castor.jolicode.com/install" | bash

Grund: Castor soll projekt-übergreifend verfügbar sein, nicht an ein Projekt gebunden.

Anti-Pattern 3: Tasks mit Side-Effects

// FALSCH: Task modifiziert unerwartete Dinge
#[AsTask()]
function test(): void
{
    run('vendor/bin/phpunit');
    run('rm -rf var/cache'); // Unexpected side-effect!
}

// RICHTIG: Explizite Task für jede Aktion
#[AsTask()]
function test(): void
{
    run('vendor/bin/phpunit');
}

#[AsTask()]
function cleanCache(): void
{
    run('rm -rf var/cache');
}

Ausblick: Castor im PHP-Ökosystem

Castor vs. andere PHP Task Runner

ToolStärkenSchwächen
CastorModern, DX-fokussiert, leichtgewichtigNoch junge Community (1.0 seit Oktober 2025)
RoboMature, viele PluginsOOP-heavy, mehr Boilerplate
PhingXML-basiert, Ant-kompatibelVeraltete Syntax, schwer wartbar

Community und Support

Castor ist ein Open-Source-Projekt von JoliCode mit aktiver Community-Entwicklung. Die 1.0-Version signalisiert API-Stabilität mit Semantic Versioning.

Ressourcen:

Fazit: Castor als pragmatische Make-Alternative

Castor 1.0 bietet deutschen Symfony- und Shopware-Agenturen eine moderne Alternative zu Makefiles und Shell-Scripts:

  • Typsicherheit: PHP statt Bash bedeutet weniger Runtime-Fehler
  • IDE-Support: Autocomplete, Refactoring, Debugging funktionieren
  • Wartbarkeit: PHP-Entwickler verstehen Tasks ohne Bash-Kenntnisse
  • Ökosystem: Composer-Packages können direkt genutzt werden
  • Performance: Minimal langsamerer Start als Make, aber vernachlässigbar

Für Projekte im 2-10k€ Segment spart Castor Zeit bei:

  • Onboarding neuer Entwickler (keine Bash-Schulung nötig)
  • Debugging von Build-Prozessen (Stack-Traces statt kryptische Errors)
  • Cross-Platform-Development (Windows-Entwickler benötigen kein WSL mehr)

Empfehlung: Für neue Symfony/Shopware-Projekte ist Castor einen Versuch wert. Migration bestehender Makefiles ist in 2-4 Stunden machbar. Die verbesserte Developer Experience zahlt sich besonders in Teams mit Junior-Entwicklern oder wechselnden Freelancern aus.

Nächste Schritte:

  1. Castor installieren: curl "https://castor.jolicode.com/install" | bash
  2. In bestehendem Projekt testen: castor.php mit 2-3 häufigen Tasks anlegen
  3. Team-Feedback einholen nach 1 Woche Nutzung
  4. Bei positivem Feedback: Makefile komplett migrieren

Quellen:

Autor: Dennis Schwenker-Sanders | d-schwenker.de

Artikel teilen: