madalynn/plum
Plum is a Laravel/PHP package by madalynn for handling money and currency values. It provides Eloquent casts, validation rules, and helpers to store, format, and convert monetary amounts safely and consistently, helping Laravel applications avoid common floating-point and formatting issues.
Plum is a PHP deployment library that models a deploy as an ordered list of Task objects executed against one or more Servers for an Application. It predates Envoy/Deployer and is not Laravel-aware, so it slots in as a standalone deployer binary/script that shells into your app over SSH.
1. Install
composer require madalynn/plum --dev
Check composer.json in the package first: Plum is an older codebase, so its PHP and Symfony constraints may not resolve cleanly against a modern Laravel app (PHP 8.1+). If resolution fails, either pin it in a separate deploy/ Composer project, or run it from a small Docker image with the matching PHP version rather than fighting constraints in your app's composer.json.
2. Read in this order
README.md — the wiring of Deployer, Server, Application, and task registration.src/ — especially the Task namespace; that's where the built-in task catalogue lives and where you'll copy patterns from.tests/ and any examples/ — the fastest way to see a real task implementing the interface.3. Your first use case
A single-server, release-directory deploy of a Laravel app:
releases/<timestamp>.composer install --no-dev --optimize-autoloader.php artisan migrate --force.php artisan config:cache && php artisan route:cache && php artisan view:cache.php artisan queue:restart (or horizon:terminate).current → the new release.current/public.4. Minimal script
Create deploy.php at the repo root (kept out of the Laravel autoload path):
<?php
require __DIR__.'/vendor/autoload.php';
use Plum\Deployer;
use Plum\Server;
use Plum\Application;
$deployer = Deployer::create();
$deployer->addServer(new Server('web1.example.com', 'deploy', 22));
$deployer->addApplication(new Application(
'myapp',
'[email protected]:acme/myapp.git',
'/var/www/myapp',
'main'
));
// Register your tasks in execution order, then run.
$deployer->run([
new App\Deploy\CreateReleaseTask(),
new App\Deploy\CheckoutTask(),
new App\Deploy\ComposerInstallTask(),
new App\Deploy\MigrateTask(),
new App\Deploy\SymlinkTask(),
new App\Deploy\ArtisanCacheTask(),
new App\Deploy\QueueRestartTask(),
]);
(Exact class names and signatures vary by version — copy the constructor shape from the built-in tasks in src/ before writing your own.)
Run it locally or from CI: php deploy.php.
One class per step. Implement the package's task interface (getName(), getDescription(), run(Server, Application) in most versions). Keeping tasks single-purpose makes them reusable across staging and production and makes failures obvious in the output.
final class MigrateTask implements TaskInterface
{
public function getName(): string { return 'artisan:migrate'; }
public function run(Server $server, Application $application): void
{
$server->run(sprintf(
'cd %s/current && php artisan migrate --force',
$application->getPath()
));
}
}
Pipeline composition. Build the task list from a config array so --env=staging swaps servers and task subsets without duplicating the script:
$config = require __DIR__."/deploy.{$env}.php";
foreach ($config['servers'] as $s) { $deployer->addServer(new Server(...$s)); }
$deployer->run(array_map(fn ($t) => new $t(), $config['tasks']));
Release-directory + symlink swap. Plum does not give you zero-downtime out of the box; you implement it as three tasks: CreateRelease (timestamped dir), PopulateRelease (git/composer/artisan), ActivateRelease (atomic ln -sfn). Keep 3–5 releases and prune older ones in a CleanupTask.
Laravel-specific tasks worth writing once
ArtisanCacheTask — config:cache, route:cache, view:cache after symlink, not before.HorizonTerminateTask / QueueRestartTask — must run after activation so workers pick up the new code.StorageLinkTask — php artisan storage:link is idempotent but noisy; guard with [ -L public/storage ] ||.PermissionsTask — chown -R www-data:www-data storage bootstrap/cache and chmod -R ug+rwX.MaintenanceTask — php artisan down --render=errors::503 before migrations, up after activation.Integrating with Laravel day-to-day
require-dev or a sibling deploy/ project). It should never be autoloaded by the application."scripts": { "deploy": "php deploy.php --env=production" }
php deploy.php after tests pass; cache the Composer/SSH directories the deployer uses.ssh-agent forwarded in CI) rather than embedding keys in the script.Multi-server fan-out. Register several Server objects and let Plum iterate. If you need "run migrations on exactly one server," split into two run() calls: one with the primary server for MigrateTask, one with all servers for activation.
Secrets. Never put credentials in the task list. Read from .env/CI variables and pass into Server/Application constructors; ensure the deployed .env lives outside the release directory (e.g. shared/.env symlinked into each release) so deployments don't clobber it.
Rollback by convention. Store a releases.txt or read the sorted release dirs to find the previous one; a RollbackTask re-points current and re-runs cache/queue tasks. Test it before you need it.
Package age and compatibility. Plum is a small, old library (86 stars, 0 dependents). Expect tight symfony/process/symfony/console constraints and possible PHP 8 incompatibilities. Isolate it in a deploy/ Composer project or a legacy PHP container if the app is on a modern runtime. Budget time to vendor a fork if you hit hard constraints.
No Laravel bindings. There is no service provider, facade, or artisan command. Everything is your glue — the deploy script, the task classes, the config loader. That's fine for a deployer, but don't expect package-provided artisan deploy.
SSH is the whole model. Tasks generally run remote commands via the Server object. Diagnose failures by copying the exact command and running it over ssh yourself. Add set -euo pipefail inside compound commands so a silent failure in composer install doesn't get masked by a successful cd.
Idempotency matters. Tasks run every deploy. Guard expensive or destructive steps (git clone when dir exists, ln -sfn, migrate is fine but migrate:fresh is not). Prefer "check then act" over "always do."
Ordering is load-bearing. Cache-clear before activation, queue restart after, migrations before activation. Keep the task list short and explicit; don't hide ordering inside a "deploy" mega-task.
Zero-downtime is DIY. The symlink swap is atomic, but PHP-FPM may hold old workers and opcache may serve stale paths briefly. Reload FPM (sudo systemctl reload php8.2-fpm) or opcache_reset as an explicit task if you see stale code.
Permissions and ownership. Deploying as a non-www-data user routinely breaks storage/ and bootstrap/cache. Add an explicit permissions task and run it after activation. Watch umask on the release directory.
.env handling. Laravel reads .env at runtime from the release dir. Symlink shared/.env into every release; never let git clean or a template overwrite it. Same for storage/ and public/uploads if applicable.
Error output is limited. Plum's console output is thinner than Deployer's. Wrap each task in try/catch, log the remote exit code and stderr, and fail loudly (exit 1) so CI stops. There is no built-in rollback on failure — add a try/catch around run() that triggers your RollbackTask.
Debugging workflow. Reproduce against the smallest target: one staging server, one task at a time ($deployer->run([new MigrateTask()])). Keep a --dry-run flag in your own wrapper that prints commands instead of executing.
Extension points. The clean seams are: custom TaskInterface implementations, a custom Deployer subclass (to override execution/ordering), and event/listener hooks if your version exposes them. Avoid forking the package for small behavior changes — a subclass or a wrapper task is usually enough.
Long-term risk. With ~86 stars and no dependents, treat Plum as unmaintained. Decide up front whether you're inheriting an existing deploy pipeline (fine) or choosing a tool (Deployer, Envoy, or Forge are the boring, supported choices). If you must keep Plum, pin the exact Composer version and document the fork/PHP constraint in your runbook.
How can I help you explore Laravel packages today?