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

Silly Laravel Package

mnapoli/silly

Silly is a micro-framework for building PHP command-line applications on top of Symfony Console. Commands are defined with simple callables and signatures like 'greet [name] [--yell]', while dependency injection, console helpers, and PHP-DI or Pimple integration are included.

View on GitHub
Deep Wiki
Context7

Getting Started

Silly is the shortest path from "I keep running this script by hand" to a real console binary. For a Laravel developer the mental model is: Artisan, minus the framework, with the command class deleted.

Install and scaffold a binary (5 minutes):

composer require mnapoli/silly
mkdir -p bin
#!/usr/bin/env php
<?php
// bin/tool
require __DIR__.'/../vendor/autoload.php';

use Silly\Application;
use Symfony\Component\Console\Output\OutputInterface;

$app = new Application('mytool', '1.0.0');

$app->command('greet [name] [--yell]', function ($name, $yell, OutputInterface $output) {
    $text = $name ? "Hello, $name" : 'Hello';
    $output->writeln($yell ? strtoupper($text) : $text);
});

$app->run();
chmod +x bin/tool
./bin/tool greet john --yell

Where to look first — in this order:

  1. docs/command-definition.md — the signature syntax ([name], [name=], [name=*], [--flag], [--opt=], [--opt=*]). This is the whole API surface you'll use daily.
  2. docs/command-callables.md — how parameters, options, services, InputInterface, OutputInterface, and SymfonyStyle get injected into the closure.
  3. docs/helpers.md — SymfonyStyle, tables, progress bars, runCommand().
  4. docs/dependency-injection.md — only when you outgrow plain closures.

First real use case for a Laravel shop: a repo that isn't a Laravel app (a shared package, a provisioning repo, a data-import tool, a bin/ shipped with a library). Booting Laravel for a 200ms task is the smell that says "use this instead." Second use case: a throwaway workflow you want to validate as a CLI before deciding whether it deserves an Artisan command and a UI.

Implementation Patterns

Thin command, thick service. Keep the closure to argument marshalling and IO; put the logic in injectable classes. This is the pattern that survives contact with code review.

$app->command('import [file] [--dry-run]', function (string $file, bool $dryRun, Importer $importer, SymfonyStyle $io) {
    $result = $importer->run($file, $dryRun);
    $io->success(sprintf('%d rows, %d skipped', $result->imported, $result->skipped));
    return $result->hasErrors() ? 1 : 0;   // exit code is honoured
});

Register services once, resolving by type-hint:

$container = new DI\Container();          // or any PSR-11 container
$app->useContainer($container);

Group commands across files. Silly has no module system; use a plain function per domain and call it from the binary:

// src/Commands/DbCommands.php
return function (Application $app): void {
    $app->command('db:drop', ...);
    $app->command('db:create', ...);
};

// bin/tool
(require __DIR__.'/../src/Commands/DbCommands.php')($app);

Compose commands. Inside a command, call siblings — this is how you build deploy out of pull/migrate/cache:clear without shelling out:

$app->command('init', function (OutputInterface $output) use ($app) {
    $app->runCommand('db:drop --force', $output);
    $app->runCommand('db:create', $output);
    $app->runCommand('db:fixtures --verbose', $output);
});

Enrich the command object. command() returns a Symfony Command, so the fluent setters keep working:

$app->command('greet [name] [--age=]', $callable)
    ->descriptions('Say hello', ['--age' => 'Years of the person'])
    ->defaults(['age' => 25])
    ->setAliases(['hi']);

Defaults can also be inferred from the PHP signature: function ($name = 'John', $age = 25). Prefer this — one source of truth.

Single-purpose binaries. $app->setDefaultCommand('greet', true) (the true makes it run without a subcommand) turns bin/tool john into the whole interface.

Testing. Because Silly\Application extends Symfony\Console\Application, the Symfony testers work unchanged:

$app = new Application();
$app->command(...);
$app->setAutoExit(false);
$tester = new ApplicationTester($app);
$tester->run(['command' => 'import', 'file' => 'x.csv', '--dry-run' => true]);
$this->assertSame(0, $tester->getStatusCode());
$this->assertStringContainsString('0 rows', $tester->getDisplay());

Distribution. Add "bin": ["bin/tool"] to composer.json so consumers get a vendor/bin/tool.

Interop with a Laravel app. Two clean shapes: (a) Silly binary that does not boot Laravel, talking to the app over HTTP/DB/queue; (b) Silly binary that boots the framework deliberately and then reuses Eloquent/services — acceptable for ops tooling where connection config must come from .env. Do not install Silly purely to replace Artisan inside an app; that's what Laravel Zero is for.

Gotchas and Tips

  • Static callables to non-static methods throw. $app->command('greet', ['MyClass', 'foo']) raises an exception by design. Use [new MyClass, 'foo'] or let the container build it.
  • Hyphens are camelCased. [--dry-run] binds to $dryRun, [number-of-clicks] to $numberOfClicks. Silently getting null almost always means a name mismatch here.
  • Options with optional values (--option=?) were removed as unusable. Model "flag or value" as two options, or a required option with a default via ->defaults().
  • Options that take values need = in the signature. [--age] is a boolean flag; [--age=] takes a value; [--tag=*] takes an array. This trips up everyone once.
  • Resolution order for closure parameters: input arguments/options by name first, then remaining parameters by type-hint from the container. A parameter named after a container entry can be shadowed by an option of the same name — rename one.
  • Don't rely on $this inside closures. Capture $app explicitly with use ($app); the binding is not something to design around.
  • Keep $app->run() inside the binary, not in an includable file. Requiring a file that calls run() from a test or another script is the classic first-day bug. Symfony's --no-interaction, -v/-vvv, -q, --ansi all come free — use $input->isInteractive() before prompting, or CI will hang.
  • Exit codes are returned, not thrown. return 1; from the closure propagates. With setAutoExit(false) you own the exit(); with the default, Symfony calls it for you.
  • Debugging: $app->setCatchExceptions(false) before run() to get raw stack traces instead of the pretty error box; -vvv for verbosity-gated logging.
  • Version drift with Laravel. Silly 1.10 supports symfony/console 8, 1.9 supports 7; a Laravel app pins its own console version. Before adding Silly to an existing app run composer why symfony/console — a bump here can affect Artisan's behaviour, and vice versa. Standalone binaries are immune.
  • No config, no .env, no helpers. Silly gives you container injection and console IO; it deliberately gives you nothing for config, logging, or dotenv. Bring vlucas/phpdotenv and your own config/ loader, or accept env vars only.
  • Laravel idioms don't exist inside the closure. No $this->argument(), $this->info(), app(), config(). You get plain typed parameters, SymfonyStyle, and whatever the container resolves.
  • Escape hatch at any time. Anything from the Symfony Console docs applies: custom HelperSet entries, $app->add(new MyCommand()) for heavy commands, overriding getDefaultCommands() to strip list/help from a shipped binary. Migrating a closure to a Command class is localised and requires no changes elsewhere.
  • Extension points worth knowing early: useContainer() (DI), setDefaultCommand($name, true) (single-command UX), and returning ints (CI-friendliness). Everything else you'll need is Symfony's, which is the point — the package is 900 stars precisely because it stays small and delegates.
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.
likyly/sdk
tdt/input
coreation/php-shapefile
coreation/gisconverter
primix/widgets
primix/notifications
primix/actions
primix/tables
primix/details
primix/forms
primix/support
marcelgwerder/laravel-api-handler
firebrikdotcom/conveyor-laravel-broadcaster
kanata-php/conveyor-laravel-broadcaster
alish/short-message
sonrac/lumen-league-oauth2
pushery/sanctum-hardening-for-laravel
evanschleret/laravel-uuidable
yigit-serin/laravel-openvidu
adachsoft/command-executor-lib