Castor Documentation

repository·main·Indexed 19 days ago

https://github.com/jolicode/castor

A lightweight, DX-oriented task runner for PHP that allows developers to write automation, CI/CD, and DevOps scripts using pure PHP functions instead of Bash, Makefiles, or YAML. It features a CLI tool, task definition via the #[AsTask] attribute, and built-in helper functions such as run(), io(), watch(), fs(), and notify().

Tokens
39.5K
Snippets
154
Records
186
Agent score
67%

What's inside Castor

  1. Review real-world Castor use-cases

    main

    Castor is used across various types of projects, from internal development tools to CLI applications and CI/CD automation. Examples of real-world implementations include:

    • Self-hosting: The Castor repository itself is developed using Castor.
    • Project Scaffolding: jolicode/docker-starter uses Castor tasks to encapsulate PHP and Docker setup.
    • CLI Tooling: monsieurbiz/SyliusPluginMaker uses Castor to power a CLI application for creating Sylius plugins, and lyrixx/twig-include-syntax uses it for a Twig syntax fixer.
    • Docker Automation: redirectionio/docker-example uses Castor tasks to build, run, and test Docker examples.
    • WASM/Deployment: jolicode/JoliTypo uses Castor to compile a PHP demo application into WASM and host it on GitHub Pages.
    • Compliance: Custom tasks can be used for project maintenance, such as checking Composer licenses.
  2. Define task arguments and options

    main

    In Castor, when you define a function to be used as a task, all function parameters are automatically treated as arguments or options.

    • Arguments: Positional parameters in the function signature.
    • Options: Parameters that can be passed via flags (e.g., --option=value).

    By default, Castor validates that all required arguments are provided and that no unknown options are passed.

    namespace arguments;
    
    function simple(string $foo, string $bar): void
    {
        echo $foo . ' ' . $bar;
    }

    Usage:

    $ castor simple foo bar
    # Output: foo bar
  3. Understand Castor's versioning scheme

    main

    Castor follows Semantic Versioning (MAJOR.MINOR.PATCH) to communicate the impact of updates:

    • MAJOR version: Incremented for incompatible API changes.
    • MINOR version: Incremented for adding functionality in a backwards-compatible manner.
    • PATCH version: Incremented for backwards-compatible bug fixes.
  4. Manage AI agent detection and output

    main

    Castor automatically detects if it is running inside an AI agent environment (such as Claude Code, GitHub Copilot Workspace, or any environment where STDIN is not a TTY). In these environments, Castor suppresses the ASCII logo and update reminders to save context window space.

    If this detection causes issues (e.g., in a CI pipeline that is misidentified as an agent), you can disable the detection by setting the CASTOR_DISABLE_AGENT_DETECTION environment variable to any non-empty value.

    CASTOR_DISABLE_AGENT_DETECTION=true castor my-task
  5. Handle FunctionsResolvedEvent in multi-mount environments

    main

    The Castor\Event\FunctionsResolvedEvent is dispatched once per mount. If your project mounts other applications, this event will trigger for the root application AND for every single mounted application.

    To ensure a listener only runs once for the main application (and ignores mounted ones), check the isRootMount property on the event object.

    Use the mountPath property if you need to identify the specific filesystem path of the mount currently being resolved.

    use Castor\Attribute\AsListener;
    use Castor\Event\FunctionsResolvedEvent;
    
    #[AsListener(event: FunctionsResolvedEvent::class)]
    function on_functions_resolved(FunctionsResolvedEvent $event): void
    {
        if (!$event->isRootMount) {
            // Skip mounted applications, only run for the root application
            return;
        }
    
        // Custom logic that must run only once
    }
  6. When to use mount() vs import()

    main

    Choosing between mount() and import() depends on how you manage working directories in your tasks:

    • Use mount() if the tasks in the target application frequently need to run relative to their own directory. mount() automatically sets the working directory to the mounted application's path, saving you from repeatedly using context()->withWorkingDirectory(__DIR__) in every task.
    • Use import() if the target application does not rely on a specific working directory or if you prefer to manage execution contexts manually.
    // If you find yourself doing this in every task of an imported app:
    #[AsTask()]
    function foobar() {
        run($command, context: context()->withWorkingDirectory(__DIR__));
    }
    
    // ...then you should use mount() instead:
    mount('path/to/app');
  7. How the `task()` function behaves with nested calls

    main

    The task() function always returns the Symfony Command object of the task currently being executed by the user, not the task where task() is called.

    If you call a task foo() from within another task bar(), task()->getName() will return bar.

    Handling missing tasks: If task() is called during an event listener or context initialization before a task is fully active, it will throw an exception. To prevent this, use task(true) to allow the function to return null instead of throwing an exception.

    #[AsTask()]
    function foo(): void
    {
        // This will return the name of the task that CALLED foo()
        io()->title(task()->getName());
    }
    
    #[AsTask()]
    function bar(): void
    {
        foo();
    }
    // Running `castor bar` outputs 'bar'
  8. How namespaces affect task names

    main

    Castor uses PHP namespaces to organize tasks into hierarchical namespaces. If a task is defined within a namespace, the task name is prefixed by that namespace. Multiple levels of namespaces are joined using a colon (:).

    For example, a function hello() inside namespace usage; becomes the task usage:hello. A function inside namespace usage:with:long; becomes usage:with:long:hello.

    namespace usage;
    use Castor\Attribute\AsTask;
    
    #[AsTask]
    function hello()
    {
        echo "Hello from castor.\n";
    }
    $ castor usage:hello
    Hello from castor.
  9. Control overwriting of existing archives

    main

    By default, the zip functions will throw an exception if the destination file already exists. You can control this behavior using the overwrite named parameter.

    • To prevent overwriting: Omit the overwrite parameter (default behavior).
    • To allow overwriting: Pass overwrite: true.
    // Will throw an exception if destination.zip already exists
    zip($source, 'destination.zip', 'password');
    
    // Will overwrite destination.zip if it already exists
    zip($source, 'destination.zip', 'password', overwrite: true);
  10. When to use Castor vs. Symfony Console

    main

    While Castor is built on top of Symfony Console, they serve different purposes:

    • Symfony Console: Best for building formal CLI applications that are part of your business logic.
    • Castor: Best for running small, operational tasks (1-2 lines of code) that simplify development, such as running Docker commands, database migrations, or clearing caches.

    Using Castor prevents your main application from being cluttered with operational commands that are not strictly related to business logic.

  11. Determine context selection precedence

    main

    When running a task, Castor determines which context to use based on the following order of precedence (highest to lowest):

    1. --context CLI option
    2. CASTOR_CONTEXT environment variable
    3. .castor.context file
    4. The context marked default: true in your code.

    Environment Variable Override:

    CASTOR_CONTEXT=my_context castor foo

    Using a .castor.context file: Create a file named .castor.context at your project root (next to castor.php). The file should contain exactly one line with the name of the context you want to use.

    my_context