Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Castor Laravel Package

jolicode/castor

Castor is a PHP-based task runner and automation tool. Define tasks as simple PHP functions and run them from the CLI, with built-in helpers for common workflows like Docker, SSH, file handling, and process management.

View on GitHub
Deep Wiki
Context7

Getting Started

Castor is a PHP task runner (Think "Makefile written in PHP, with Symfony Console ergonomics"). For a Laravel codebase it is not a replacement for Artisan — it sits one level above it, orchestrating Artisan, Composer, npm, Docker/Sail, and CI steps that today live in scattered shell scripts, README snippets, and GitHub Actions YAML.

Minimal setup:

# Prefer a dev dependency over a global install: version is pinned in composer.lock,
# available in CI, and identical for every teammate.
composer require --dev jolicode/castor

Create castor.php in the project root (Castor looks for this file by default when you run castor from the directory):

<?php

use Castor\Attribute\AsTask;

use function Castor\run;

#[AsTask(description: 'Run the full local quality gate')]
function qa(): void
{
    run(['vendor/bin/pint', '--test']);
    run(['vendor/bin/phpstan', 'analyse', '--memory-limit=1G']);
    run(['php', 'artisan', 'test']);
}

#[AsTask(description: 'Fresh local environment')]
function bootstrap(): void
{
    run(['composer', 'install']);
    run(['npm', 'ci']);
    run(['cp', '.env.example', '.env']);   // guard with fs()->exists() in real code
    run(['php', 'artisan', 'key:generate']);
    run(['php', 'artisan', 'migrate', '--seed']);
    run(['npm', 'run', 'build']);
}

Verify with castor list and castor qa.

Where to look first, in this order:

  1. castor list / castor <task> --help — Castor maps function parameters to CLI arguments and options automatically, so the help output is the real API.
  2. vendor/jolicode/castor/src/functions.php — the authoritative list of available functions (run, parallel, capture, fs, io, log, variable, context, import, watch, notify, fingerprint, …). Docs at castor.jolicode.com cover the same ground but lag behind the source.
  3. The examples/ directory in the package (and the castor.php files of JoliCode's own projects) — realistic, copy-pasteable patterns.

First use case worth shipping on day one: a castor qa task that is byte-for-byte what CI runs. That single task eliminates the "works on my machine" class of PR failures and gives new hires one command to learn.

Implementation Patterns

Treat castor.php as a router, not a script. Keep it small and import() per-domain files:

// castor.php
use function Castor\import;

import(__DIR__ . '/castor/qa.php');
import(__DIR__ . '/castor/docker.php');
import(__DIR__ . '/castor/release.php');

Each file declares its own #[AsTask] functions. import() paths resolve relative to the calling file, and task names can be grouped with the namespace: argument on #[AsTask] so you get castor docker:up, castor docker:logs.

Wrap Artisan with the array form of run(). run('php artisan migrate') goes through a shell; run(['php', 'artisan', 'migrate']) does not. The array form is the only one that behaves identically on Linux, macOS, and Windows-with-WSL, and it removes a whole class of quoting bugs around paths with spaces.

Use context() instead of repeating flags. A Sail/Docker context or a --env=testing context defined once and passed to run(context: $ctx) keeps tasks readable and prevents the "one task still runs on the host" mistake.

Extract shared logic into plain PHP functions. Tasks are thin wrappers; helpers do the work. Calling another task via run(['castor', 'other-task']) spawns a subprocess and loses type safety — call the function directly instead.

Parallelize the slow, independent parts. parallel() is the single biggest DX win in a Laravel repo: Pint, PHPStan, and Pest have no ordering dependency, so they should not run serially. Collect the processes and wait() on them; use capture() for anything whose output you need to inspect, and notify() when the run finishes so you can context-switch away from the terminal.

Watch mode for TDD. watch('app', fn () => run(['php', 'artisan', 'test', '--filter=...'])) gives you a Pest-watcher without adding another npm dependency.

Boot Laravel only when a task actually needs it. For anything that needs config() or the container, require __DIR__ . '/vendor/autoload.php' and bootstrap the application inside the task function, lazily. Doing it at the top of castor.php makes every task — including castor list — pay the boot cost and load a stale .env.

Keep the CI/local contract explicit. One castor ci task, invoked both locally and from GitHub Actions (install the binary, cache Composer, then castor ci). Guard against drift with guard_min_version() / fingerprint() at the top of the file so a stale local Castor fails loudly instead of silently behaving differently.

Where Castor should not go: anything that is a single Artisan command. php artisan migrate:fresh --seed does not need a Castor wrapper. Castor earns its keep only when a task spans two or more tools, runs things in parallel, or is a multi-step release procedure.

Gotchas and Tips

  • No Laravel bootstrap, no facades. config(), DB::, and the container are all unavailable by default. This trips up nearly everyone once. Either shell out to php artisan tinker --execute="...", or bootstrap the app explicitly inside the task.
  • .env is not loaded. Castor reads the process environment, not Laravel's dotenv. Use variable() with an env fallback for values a task needs, and never hardcode credentials in castor.php.
  • run() throws on non-zero exit. Great for CI, noisy for exploratory tasks. Use the allow_failure option where a step is expected to fail (e.g. phpstan baseline generation) and surface getErrorOutput() yourself for a clean message.
  • parallel() needs pcntl/posix. Not available on native Windows. Either require WSL or fall back to serial execution via a --no-parallel-style option. This is the most common "works for me" bug report on a mixed-OS team.
  • Interleaved output in parallel runs. Three processes writing to one TTY is unreadable. Either capture() and print the summaries at the end, or accept it and rely on exit codes.
  • Every run(['php', 'artisan', ...]) is a fresh Laravel boot. Chaining ten Artisan calls costs ten boots. Batch where possible (artisan migrate --seed && artisan ... in one array via a shell, or a single custom Artisan command) — this matters on CI minutes.
  • import() is not idempotent in the way you expect. Importing the same file twice registers duplicate tasks and produces confusing list output. Keep the import graph flat and one-directional.
  • Task naming. Function names are exposed as task names, with underscores and namespaces mapped to :-separated names. Rename a function and you silently break every README, CI job, and teammate's shell alias that referenced the old name — treat task names as public API and add aliases: when renaming.
  • Pin the version. Castor's DSL has moved across releases; a globally installed binary on someone's machine can be a different major than what the repo expects. The dev-dependency-plus-guard_min_version() combination is the fix. Commit composer.lock.
  • Debugging. castor -vvv <task> shows the underlying Symfony Process invocations; castor list is the fastest way to confirm a newly imported file actually registered its tasks (import() failures can be quiet).
  • Extension points worth knowing. #[AsTask] for registration, #[AsContext] for reusable contexts, and #[AsListener] for ProcessStartEvent / ProcessTerminateEvent, which is where you hook timing telemetry or a Slack ping for release tasks. Custom functions live in any imported PHP file — you are not limited to the built-ins.
  • Adoption advice. Introduce Castor with one task (qa), not with a migration of every script. A castor.php that duplicates composer.json scripts and Artisan commands is pure overhead, and it is the most likely reason a team abandons the tool after a sprint.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo