Skip to content
Open
60 changes: 52 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
# Jobby, a PHP cron job manager #
[![Total Downloads](https://img.shields.io/packagist/dt/hellogerard/jobby.svg)](https://packagist.org/packages/hellogerard/jobby)
[![Latest Version](https://img.shields.io/packagist/v/hellogerard/jobby.svg)](https://packagist.org/packages/hellogerard/jobby)
[![Build Status](https://img.shields.io/travis/jobbyphp/jobby.svg)](https://travis-ci.org/jobbyphp/jobby)
[![MIT License](https://img.shields.io/packagist/l/hellogerard/jobby.svg)](https://github.com/jobbyphp/jobby/blob/master/LICENSE)
[![Total Downloads](https://img.shields.io/packagist/dt/hatimox/jobby.svg)](https://packagist.org/packages/hatimox/jobby)
[![Latest Version](https://img.shields.io/packagist/v/hatimox/jobby.svg)](https://packagist.org/packages/hatimox/jobby)
[![Build Status](https://img.shields.io/travis/hatimox/jobby.svg)](https://travis-ci.org/hatimox/jobby)
[![MIT License](https://img.shields.io/packagist/l/hatimox/jobby.svg)](https://github.com/hatimox/jobby/blob/master/LICENSE)

Install the master jobby cron job, and it will manage all your offline tasks. Add jobs without modifying crontab.
Jobby can handle logging, locking, error emails and more.

**NEW REPO:** We have moved `jobby` to a Github org. Please update your remotes to `https://github.com/jobbyphp/jobby.git`.

## Features ##

- Maintain one master crontab job.
Expand All @@ -19,14 +17,21 @@ Jobby can handle logging, locking, error emails and more.
- Run job as another user, if crontab user has `sudo` privileges.
- Run only on certain hostnames (handy in webfarms).
- Theoretical Windows support (but not ever tested)
- Send alerts to Slack or Mattermost whenever a job exits with an error status.

## Requirements ##

- PHP 8.0 or higher
- [Composer](http://getcomposer.org)
- `posix` extension (for Unix/Linux systems)

## Getting Started ##

### Installation ###

The recommended way to install Jobby is through [Composer](http://getcomposer.org):
```
$ composer require hellogerard/jobby
$ composer require hatimox/jobby
```

Then add the following line to your (or whomever's) crontab:
Expand All @@ -36,7 +41,7 @@ Then add the following line to your (or whomever's) crontab:

After Jobby installs, you can copy an example file to the project root.
```
$ cp vendor/hellogerard/jobby/resources/jobby.php .
$ cp vendor/hatimox/jobby/resources/jobby.php .
```

### Running a job ###
Expand Down Expand Up @@ -208,18 +213,57 @@ dateFormat | string | Y-m-d H:i:s | Format for da
_**Mailing**_ | | | _**Options for emailing errors**_
recipients | string | null | Comma-separated string of email addresses
mailer | string | sendmail | Email method: _sendmail_ or _smtp_ or _mail_
mailSubject | string | null | Email subject
smtpHost | string | null | SMTP host, if `mailer` is smtp
smtpPort | integer | 25 | SMTP port, if `mailer` is smtp
smtpUsername | string | null | SMTP user, if `mailer` is smtp
smtpPassword | string | null | SMTP password, if `mailer` is smtp
smtpSecurity | string | null | SMTP security option: _ssl_ or _tls_, if `mailer` is smtp
smtpSender | string | jobby@<hostname> | The sender and from addresses used in SMTP notices
smtpSenderName | string | Jobby | The name used in the from field for SMTP messages
_**Notifications**_ | | | _**Options for sending Alerts when errors**_
mattermostUrl | string | null | The webhook url from Mattermost
slackChannel | string | null | The name of Slack Channel (#channel)
slackUrl | string | null | The webhook url from Slack
slackSender | string | null | The name used in the from field for Slack

## Symfony integration ##

Symfony bundle for Jobby - [imper86/jobby-cron-bundle](https://github.com/imper86/jobby-cron-bundle)

## Changelog ##

### v3.0.0 (PHP 8+ Upgrade) ###

**Breaking Changes:**
- Minimum PHP version is now 8.0
- Replaced SwiftMailer with Symfony Mailer
- PHPUnit upgraded to ^9.0|^10.0|^11.0

**New Features & Improvements:**
- Full PHP 8.0+ compatibility with typed properties and return types
- Uses Symfony Mailer DSN format for mail configuration
- Modern PHP syntax: union types, match expressions, named arguments support
- Strict type comparisons throughout the codebase

**Migration Guide:**

If you were injecting a custom mailer, update your code:

```php
// Before (SwiftMailer)
$mailer = new \Swift_Mailer($transport);
$helper = new \Jobby\Helper($mailer);

// After (Symfony Mailer)
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport;

$transport = Transport::fromDsn('smtp://localhost:25');
$mailer = new Mailer($transport);
$helper = new \Jobby\Helper($mailer);
```

## Credits ##

Developed before, but since inspired by [whenever](<https://github.com/javan/whenever>).
Expand Down
26 changes: 12 additions & 14 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,30 +1,28 @@
{
"name": "hellogerard/jobby",
"homepage": "https://github.com/jobbyphp/jobby",
"name": "hatimox/jobby",
"homepage": "https://github.com/hatimox/jobby",
"license": "MIT",
"description": "Manage all your cron jobs without modifying crontab. Handles locking, logging, error emails, and more.",
"authors": [
{
"name": "Gerard Sychay",
"email": "hellogerard@gmail.com",
"homepage": "https://github.com/hellogerard"
},
{
"name": "Michael Contento",
"homepage": "https://github.com/michaelcontento"
"name": "Hatim HAFFANE",
"email": "hatim.haffane@gmail.com",
"homepage": "https://github.com/hatimox"
}
],
"require": {
"php": ">=5.6",
"php": ">=8.0",
"alek13/slack": "^2.2",
"dragonmantank/cron-expression": "^3.0",
"guzzlehttp/guzzle": "^7.0",
"opis/closure": "^3.5",
"swiftmailer/swiftmailer": "^5.4|^6.0",
"symfony/process": "^2.7|^3.0|^4.0|^5.0"
"symfony/mailer": "^5.4|^6.0|^7.0",
"symfony/process": "^5.0|^6.0|^7.0"
},
"require-dev": {
"mp091689/dump-die": "^1.0",
"phpunit/phpunit": "^4.6",
"symfony/filesystem": "^2.7|^3.0|^4.0|^5.0"
"phpunit/phpunit": "^9.0|^10.0|^11.0",
"symfony/filesystem": "^5.0|^6.0|^7.0"
},
"autoload": {
"psr-4": {
Expand Down
105 changes: 57 additions & 48 deletions src/BackgroundJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,29 @@

namespace Jobby;

use Opis\Closure\SerializableClosure;

class BackgroundJob
{
/**
* @var Helper
*/
protected $helper;
protected Helper $helper;

/**
* @var string
*/
protected $job;
protected string $job;

/**
* @var string
*/
protected $tmpDir;
protected string $tmpDir;

/**
* @var array
*/
protected $config;
protected array $config;

/**
* @param string $job
* @param array $config
* @param Helper $helper
*/
public function __construct($job, array $config, Helper $helper = null)
public function __construct(string $job, array $config, ?Helper $helper = null)
{
$this->job = $job;
$this->config = $config + [
Expand All @@ -54,7 +47,11 @@ public function __construct($job, array $config, Helper $helper = null)
'dateFormat' => null,
'enabled' => null,
'haltDir' => null,
'debug' => null,
'mattermostUrl' => null,
'slackChannel' => null,
'slackUrl' => null,
'slackSender' => null,
'mailSubject' => null,
];

$this->config['output_stdout'] = $this->config['output_stdout'] === null ? $this->config['output'] : $this->config['output_stdout'];
Expand All @@ -65,15 +62,15 @@ public function __construct($job, array $config, Helper $helper = null)
$this->tmpDir = $this->helper->getTempDir();
}

public function run()
public function run(): void
{
$lockFile = $this->getLockFile();

try {
$this->checkMaxRuntime($lockFile);
} catch (Exception $e) {
$this->log('ERROR: ' . $e->getMessage(), 'stderr');
$this->mail($e->getMessage());
$this->notify($e->getMessage());

return;
}
Expand All @@ -96,34 +93,29 @@ public function run()
$this->log('INFO: ' . $e->getMessage(), 'stderr');
} catch (Exception $e) {
$this->log('ERROR: ' . $e->getMessage(), 'stderr');
$this->mail($e->getMessage());
$this->notify($e->getMessage());
}

if ($lockAcquired) {
$this->helper->releaseLock($lockFile);

// remove log file if empty
$logfile = $this->getLogfile();
if (is_file($logfile) && filesize($logfile) <= 0) {
if ($logfile !== false && is_file($logfile) && (filesize($logfile) <= 2 || file_get_contents($logfile) === "[]")) {
unlink($logfile);
}
}
}

/**
* @return array
*/
public function getConfig()
public function getConfig(): array
{
return $this->config;
}

/**
* @param string $lockFile
*
* @throws Exception
*/
protected function checkMaxRuntime($lockFile)
protected function checkMaxRuntime(string $lockFile): void
{
$maxRuntime = $this->config['maxRuntime'];
if ($maxRuntime === null) {
Expand All @@ -143,9 +135,9 @@ protected function checkMaxRuntime($lockFile)
}

/**
* @param string $message
* @deprecated
*/
protected function mail($message)
protected function mail(string $message): void
{
if (empty($this->config['recipients'])) {
return;
Expand All @@ -158,11 +150,36 @@ protected function mail($message)
);
}

/**
* @param string $output
* @return string
*/
protected function getLogfile($output = 'stdout')
protected function notify(string $message): void
{
if (!empty($this->config['recipients'])) {
$this->helper->sendMail(
$this->job,
$this->config,
$message
);
}

if (!empty($this->config['mattermostUrl'])) {
$this->helper->sendMattermostAlert(
$this->job,
$this->config,
$message
);
}

if (!empty($this->config['slackChannel']) && !empty($this->config['slackUrl'])) {
$this->helper->sendSlackAlert(
$this->job,
$this->config,
$message
);
}


}

protected function getLogfile(string $output = 'stdout'): string|false
{
$logfile = $this->config['output_'.$output];
if ($logfile === null) {
Expand All @@ -178,10 +195,7 @@ protected function getLogfile($output = 'stdout')
return $logfile;
}

/**
* @return string
*/
protected function getLockFile()
protected function getLockFile(): string
{
$tmp = $this->tmpDir;
$job = $this->helper->escape($this->job);
Expand All @@ -195,10 +209,7 @@ protected function getLockFile()
}
}

/**
* @return bool
*/
protected function shouldRun()
protected function shouldRun(): bool
{
if (!$this->config['enabled']) {
return false;
Expand All @@ -211,18 +222,14 @@ protected function shouldRun()
}

$host = $this->helper->getHost();
if (strcasecmp($this->config['runOnHost'], $host) != 0) {
if (strcasecmp($this->config['runOnHost'], $host) !== 0) {
return false;
}

return true;
}

/**
* @param string $message
* @param string $output
*/
protected function log($message, $output = 'stdout')
protected function log(string $message, string $output = 'stdout'): void
{
$now = date($this->config['dateFormat'], $_SERVER['REQUEST_TIME']);

Expand All @@ -231,7 +238,7 @@ protected function log($message, $output = 'stdout')
}
}

protected function runFunction()
protected function runFunction(): void
{
$command = unserialize($this->config['closure']);

Expand All @@ -246,7 +253,9 @@ protected function runFunction()
}
$content = ob_get_contents();
if ($logfile = $this->getLogfile()) {
file_put_contents($this->getLogfile(), $content, FILE_APPEND);
if(strlen($content) > 2){
file_put_contents($this->getLogfile(), $content, FILE_APPEND);
}
}
ob_end_clean();

Expand All @@ -255,7 +264,7 @@ protected function runFunction()
}
}

protected function runFile()
protected function runFile(): void
{
// If job should run as another user, we must be on *nix and
// must have sudo privileges.
Expand Down
Loading