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
| Tool | Cold Start | Warm Start | Task-Ausführung |
| Make | 0,05s | 0,05s | 45,3s |
| npm scripts | 0,8s | 0,3s | 45,8s |
| Symfony Command | 1,2s | 0,9s | 46,1s |
| Castor | 0,15s | 0,12s | 45,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
| Tool | Stärken | Schwächen |
| Castor | Modern, DX-fokussiert, leichtgewichtig | Noch junge Community (1.0 seit Oktober 2025) |
| Robo | Mature, viele Plugins | OOP-heavy, mehr Boilerplate |
| Phing | XML-basiert, Ant-kompatibel | Veraltete 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:
- Offizielle Docs: castor.jolicode.com
- GitHub: github.com/jolicode/castor
- Packagist: packagist.org/packages/jolicode/castor
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:
- Castor installieren:
curl "https://castor.jolicode.com/install" | bash - In bestehendem Projekt testen:
castor.phpmit 2-3 häufigen Tasks anlegen - Team-Feedback einholen nach 1 Woche Nutzung
- Bei positivem Feedback: Makefile komplett migrieren
Quellen:
- Castor 1.0 Release: jolicode.com/blog
- Symfony Community Update: symfony.com/blog
- Castor Dokumentation: castor.jolicode.com
- GitHub Repository: github.com/jolicode/castor
Autor: Dennis Schwenker-Sanders | d-schwenker.de