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

Crontab Manager Laravel Package

tiben/crontab-manager

Manage server cron jobs from a Laravel web UI. Add, edit, enable, disable, and delete crontab entries, run jobs on demand, and review logs without touching the command line. Supports multiple cron types and per-job scheduling controls.

View on GitHub
Deep Wiki
Context7

Getting Started

tiben/crontab-manager is not a Laravel package — there is no service provider, no config file, no facade, no auto-discovery. It is a plain PHP wrapper around the crontab binary that shells out via exec(), parses crontab -l, and lets you add/remove job objects before writing the whole table back with crontab -. Treat it as a low-level system utility you wire up yourself.

Install and pin it (last release 2023-05-02, single-maintainer project):

composer require tiben/crontab-manager:^4.0

Confirm your environment before writing any code:

which crontab          # must resolve
crontab -l             # must be readable by the user running PHP
php -r 'var_dump(function_exists("exec"));'

Then bind the manager as a singleton and do one read-only smoke test before any writes:

// app/Providers/AppServiceProvider.php
use TiBeN\CrontabManager\CrontabAdapter;
use TiBeN\CrontabManager\CrontabManager;

$this->app->singleton(CrontabManager::class, fn () => new CrontabManager(new CrontabAdapter()));
// tinker
$crontab = app(\TiBeN\CrontabManager\CrontabManager::class);
foreach ($crontab->getJobs() as $job) {
    dump($job->name, $job->command);
}

First real use case: not running your Laravel schedule. Laravel's scheduler is already one static cron line (* * * * * cd /var/www/app && php artisan schedule:run); replacing it with this library is a downgrade. The genuinely useful case is managing entries that live outside Laravel's schedule: per-tenant dynamic jobs written at runtime, provisioning cron for a system user during deploy, or cleaning up legacy entries a previous team left behind. Start there — an artisan command like cron:sync {tenant} that upserts one named, idempotent entry — and keep it out of HTTP request paths entirely.

Implementation Patterns

The v4 API shape. CrontabManager replaces the older 3.x CrontabRepository, and setters became public properties:

use TiBeN\CrontabManager\CrontabJob;

$job = new CrontabJob();
$job->name          = 'app-tenant-42-report';           // becomes a `# name` comment line
$job->command       = '/usr/bin/php /var/www/app/artisan tenant:report 42 >> /var/www/app/storage/logs/tenant-42.log 2>&1';
$job->minute        = '0';
$job->hour          = '3';
$job->dayOfMonth    = '*';
$job->month         = '*';
$job->dayOfWeek     = '*';
$job->enabled       = true;                             // false = commented out

$crontab->add($job);
$crontab->save();                                       // rewrites the ENTIRE table

If you're on 3.x, everything reads $repo = new CrontabRepository(new CrontabAdapter()), $job->setJob(...), $repo->persist(). Pick a major version and stay there — upgrading is a rewrite, not a bump.

Idempotent upsert — the only pattern you should ship. add() does not guarantee dedupe, and this library rewrites the whole file, so a duplicate entry silently runs your job twice. Resolve by name first:

public function sync(CrontabJob $desired): void
{
    $this->crontab->replace($desired, function (CrontabJob $existing) {
        $this->crontab->remove($existing);
    });
}

// or explicitly:
foreach ($this->crontab->getJobs() as $existing) {
    if ($existing->name === $desired->name) {
        $this->crontab->remove($existing);
    }
}
$this->crontab->add($desired);
$this->crontab->save();

Namespace your jobs by name prefix. Since name is purely a comment, it's your only ownership marker. Use app-tenant-{id}-{purpose} so a cron:prune command can safely remove str_starts_with($job->name, 'app-') and never touch hand-added system entries. Never write a removal loop that clears the whole table.

Wrap it in a domain-level interface before you use it twice. The library is thin enough that you will want to swap it or fake it:

interface CrontabGateway {
    public function upsert(string $name, string $command, string $schedule): void;
    public function remove(string $name): void;
}

final class CrontabManagerGateway implements CrontabGateway { /* ... */ }

// tests / local dev
final class InMemoryCrontabGateway implements CrontabGateway { public array $jobs = []; }

This also lets you keep the package out of phpunit.xml territory — unit tests get the in-memory double, and only a tagged integration test on a real Linux box touches the binary.

Serialize writes. The read-modify-write cycle has no locking, so two concurrent save() calls will clobber each other and drop entries. Guard every write:

Cache::lock('crontab:write', 10)->block(5, function () use ($job) {
    $this->gateway->upsert($job->name, $job->command, $job->schedule);
});

Invoke it from CLI only. The user running php-fpm (www-data) almost never has a crontab, and crontab -l for that user either errors or returns empty. Run mutations from an artisan command in a deploy hook, from a queue worker (same user as artisan), or as a one-off artisan run in the release script — never from a controller or a model observer.

Keep it out of the scheduler's critical path. If a tenant needs a dynamic schedule, prefer generating the schedule inside Laravel:

// routes/console.php
foreach (Tenant::whereNotNull('report_at')->cursor() as $tenant) {
    Schedule::command("tenant:report {$tenant->id}")->cron($tenant->report_at);
}

Reach for crontab-manager only when the job must run outside the app's user context, or when you're provisioning a system/root crontab — in which case you'll be writing to /etc/cron.d/ yourself anyway, and this library doesn't help.

Gotchas and Tips

  • The user context is the single most common failure. The library manipulates the invoking user's crontab. It does not do sudo, does not write /etc/cron.d/, and does not know about system users other than the current one. A job that works from php artisan will silently fail from a controller.

  • exec() may be disabled. Shared hosts and hardened images routinely set disable_functions=exec. The adapter will throw a CrontabException that reads like a parse failure. Assert early:

    if (! function_exists('exec') || ! shell_exec('command -v crontab')) {
        throw new \RuntimeException('crontab-manager requires exec() and the crontab binary.');
    }
    
  • Full-table rewrite means formatting loss. Every save() re-serializes everything the parser understands. Hand-written alignment, comments without a following job, and unusual env lines can be reordered or dropped. Before enabling in production, do a crontab -l > before.txt, run one sync, crontab -l > after.txt, and diff. Keep a backup cron entry of your own.

  • % is special in cron. A literal percent in your command becomes a newline unless escaped as \%. Password arguments, date +%F, and URLs with encoded chars will bite you. Quote the whole command and escape defensively.

  • Cron runs /bin/sh, not bash, with a minimal PATH. Always absolute paths (/usr/bin/php, not php), no .env inheritance, no source. Laravel's artisan needs cd or --realpath; both are safest as cd /var/www/app && /usr/bin/php artisan ....

  • Uncaptured output becomes mail. Without >> /path/log 2>&1 cron tries to email it; on a container with no MTA you get silent loss. Set MAILTO="" at the top of the table or always redirect.

  • name is cosmetic. It's a comment; cron ignores it. If someone edits the file by hand, name-based lookup and removal break. Treat hand-editing as unsupported and document it.

  • License needs verification before commercial use. The repository metadata reports NOASSERTION, and upstream is widely cited as GPL-3.0. A GPL dependency in a proprietary SaaS/CLI is a legal conversation, not a code review comment. Confirm with your legal owner, or isolate the package behind a separate process/artifact.

  • Version and maintenance risk. 138 stars, zero known dependents, last release May 2023. Pin exactly ("tiben/crontab-manager": "4.0.*"), keep the surface area to two or three calls, and keep your CrontabGateway interface so replacing it with a hundred lines of exec() is a one-afternoon job.

  • Container and macOS caveats. On macOS you get BSD cron semantics; in slim Docker images crontab may not be installed (apt-get install cron) and the daemon must actually be running (cron -f) or nothing fires regardless of what the table says. Windows is unsupported.

  • Debugging tip: the temp file the library writes is the ground truth. When a save misbehaves, capture the array from getJobs() and the raw crontab -l output side by side — most "the package is broken" reports are a parser/format mismatch that the raw strings reveal immediately.

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.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums