<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker;

interface ContextData
{
    public function __construct();
}
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker\Signaller;

use PHPUnit\Framework\TestCase;

/**
 * @internal
 * @covers \HappyInc\Worker\Signaller\FileSignaller
 *
 * @small
 */
final class FileSignallerTest extends TestCase
{
    public function testNoSignalReceivedWhenNotSent(): void
    {
        $channel = 'channel';
        $signaller = new FileSignaller();
        $listener = $signaller->createListener($channel);

        $signal = $listener();

        $this->assertFalse($signal);
    }

    public function testNoSignalReceivedWhenSentBeforeCreatingListener(): void
    {
        $channel = 'channel';
        $signaller = new FileSignaller();

        $signaller->sendSignal($channel);
        $signal = $signaller->createListener($channel)();

        $this->assertFalse($signal);
    }

    public function testSignalReceivedWhenSent(): void
    {
        $channel = 'channel';
        $signaller = new FileSignaller();
        $listener = $signaller->createListener($channel);

        $signaller->sendSignal($channel);
        $signal = $listener();

        $this->assertTrue($signal);
    }
}
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker\Signaller;

final class FileSignaller implements Signaller
{
    private const MAX_LENGTH = 26;

    private string $dir;

    public function __construct(?string $dir = null)
    {
        $this->dir = rtrim($dir ?? sys_get_temp_dir(), \DIRECTORY_SEPARATOR);
    }

    public function sendSignal(string $channel): void
    {
        if (!is_dir($this->dir) && !@mkdir($this->dir, 0777, true)) {
            throw new \RuntimeException(sprintf('Failed to create directory "%s".', $this->dir));
        }

        $file = $this->channelFile($channel);

        if (false === $handle = @fopen($this->channelFile($channel), 'cb')) {
            throw new \RuntimeException(sprintf('Failed to open file "%s" for writing.', $file));
        }

        if (!flock($handle, LOCK_EX)) {
            throw new \RuntimeException(sprintf('Failed to acquire an exclusive lock for file "%s".', $file));
        }

        ftruncate($handle, 0);
        fwrite($handle, (new \DateTimeImmutable('now', new \DateTimeZone('UTC')))->format('Y-m-d H:i:s.u'));
        fflush($handle);
        flock($handle, LOCK_UN);
        fclose($handle);
    }

    public function createListener(string $channel): callable
    {
        $file = $this->channelFile($channel);
        $initialValue = @file_get_contents($file, false, null, 0, self::MAX_LENGTH);

        return static fn (): bool => $initialValue !== @file_get_contents($file, false, null, 0, self::MAX_LENGTH);
    }

    private function channelFile(string $channel): string
    {
        return sprintf('%s/%s.worker_stop_signaller_channel.tmp', $this->dir, md5($channel));
    }
}
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker\ProcessSignal;

use HappyInc\Worker\Context;
use PHPUnit\Framework\TestCase;

/**
 * @internal
 * @covers \HappyInc\Worker\ProcessSignal\ProcessSignalExtension
 *
 * @small
 */
final class ProcessSignalExtensionTest extends TestCase
{
    public function testNotStoppedWhenDifferentSignalSent(): void
    {
        $context = new Context();
        $extension = new ProcessSignalExtension([SIGINT]);
        pcntl_signal(SIGUSR1, SIG_IGN);

        $extension->started($context);
        posix_kill(posix_getpid(), SIGUSR1);
        $stop = $extension->jobDone($context, 0);

        $this->assertNull($stop);
    }

    public function testStoppedWhenSignalSent(): void
    {
        $context = new Context();
        $extension = new ProcessSignalExtension([SIGINT]);

        $extension->started($context);
        posix_kill(posix_getpid(), SIGINT);
        $stop = $extension->jobDone($context, 0);

        $this->assertNotNull($stop);
    }
}
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker\MemoryLimit;

use PHPUnit\Framework\TestCase;

/**
 * @internal
 * @covers \HappyInc\Worker\MemoryLimit\MemoryLimit
 *
 * @small
 */
final class MemoryLimitTest extends TestCase
{
    public function testFromBytes(): void
    {
        $limit = MemoryLimit::fromBytes(16);

        $this->assertSame(16, $limit->bytes);
    }

    public function testFromMegaBytes(): void
    {
        $limit = MemoryLimit::fromMegabytes(1);

        $this->assertSame(1048576, $limit->bytes);
    }

    /**
     * @dataProvider fromIniMemoryLimitProvider
     */
    public function testFromIniMemoryLimit(string $iniLimit, float $share, int $expectedLimit): void
    {
        $originalIniLimit = ini_get('memory_limit');
        ini_set('memory_limit', $iniLimit);

        $limit = MemoryLimit::fromIniMemoryLimit($share);

        $this->assertSame($expectedLimit, $limit->bytes);

        ini_set('memory_limit', $originalIniLimit);
    }

    /**
     * @psalm-return \Generator<int, array{string, float, int}>
     */
    public function fromIniMemoryLimitProvider(): \Generator
    {
        yield ['-1', 1, 100 * 1024 ** 2];
        yield ['10485760', 1, 10485760];
        yield ['10485760', 0.1, 1048576];
        yield ['10240k', 1, 10 * 1024 ** 2];
        yield ['10240K', 1, 10 * 1024 ** 2];
        yield ['10m', 1, 10 * 1024 ** 2];
        yield ['10M', 1, 10 * 1024 ** 2];
        yield ['1g', 1, 1024 ** 3];
        yield ['1G', 1, 1024 ** 3];
    }
}
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker\Signaller;

use HappyInc\Worker\ContextData;

/**
 * @internal
 * @psalm-internal \HappyInc\Worker\Signaller
 */
final class SignallerContextData implements ContextData
{
    /**
     * @var callable
     * @psalm-var callable(): bool
     */
    public $listener;

    public function __construct()
    {
        $this->listener = static fn (): bool => false;
    }
}
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker\Sleep;

/**
 * @psalm-immutable
 */
final class SleepInterval
{
    /**
     * @param positive-int $microseconds
     */
    private function __construct(public int $microseconds)
    {
    }

    /**
     * @psalm-pure
     */
    public static function fromMicroseconds(int $microseconds): self
    {
        if ($microseconds <= 0) {
            throw new \InvalidArgumentException('Sleep interval must be zero or positive.');
        }

        return new self($microseconds);
    }

    /**
     * @psalm-pure
     */
    public static function fromMilliseconds(int $milliseconds): self
    {
        if ($milliseconds <= 0) {
            throw new \InvalidArgumentException('Sleep interval must be zero or positive.');
        }

        return new self($milliseconds * 1000);
    }

    /**
     * @psalm-pure
     */
    public static function fromSeconds(int $seconds): self
    {
        if ($seconds <= 0) {
            throw new \InvalidArgumentException('Sleep interval must be zero or positive.');
        }

        return new self($seconds * 1_000_000);
    }
}
<file_sep># Happy Inc Worker

```php
use HappyInc\Worker\MemoryLimit\MemoryLimit;
use HappyInc\Worker\MemoryLimit\MemoryLimitExtension;
use HappyInc\Worker\ProcessSignal\ProcessSignalExtension;
use HappyInc\Worker\Signaller\FileSignaller;
use HappyInc\Worker\Signaller\SignallerExtension;
use HappyInc\Worker\Sleep\SleepExtension;
use HappyInc\Worker\Sleep\SleepInterval;
use HappyInc\Worker\Worker;
use Psr\Log\LogLevel;

$signaller = new FileSignaller('/some/dir');

$worker = new Worker([
    new MemoryLimitExtension(
        MemoryLimit::fromIniMemoryLimit(0.7), // stop when allocated memory reaches 70% of php.ini memory_limit
        $systemLogger,
        LogLevel::CRITICAL, // optionally log when memory limit is reached with the specified level
    ),
    new ProcessSignalExtension([SIGINT]), // gracefully stop the worker when Ctrl+C is pressed in the terminal
    new SignallerExtension('mailing_worker', $signaller), // allows to send a stop signal from a different process
    new SleepExtension(SleepInterval::fromSeconds(1)), // sleep 1 second after each job
]);

$worker->workOn(function (): void {
    // some job
});

$signaller->sendSignal('mailing_worker'); // stop all workers, listening to the "mailing_worker" channel via the SignallerExtension
```
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker;

final class Worker
{
    /**
     * @var WorkerStartedExtension[]
     */
    private array $startedExtensions = [];

    /**
     * @var WorkerJobDoneExtension[]
     */
    private array $jobDoneExtensions = [];

    /**
     * @var WorkerStoppedExtension[]
     */
    private array $stoppedExtensions = [];

    /**
     * @psalm-param iterable<WorkerStartedExtension|WorkerJobDoneExtension|WorkerStoppedExtension> $extensions
     */
    public function __construct(iterable $extensions = [])
    {
        foreach ($extensions as $extension) {
            if ($extension instanceof WorkerStartedExtension) {
                $this->startedExtensions[] = $extension;
            }

            if ($extension instanceof WorkerJobDoneExtension) {
                $this->jobDoneExtensions[] = $extension;
            }

            if ($extension instanceof WorkerStoppedExtension) {
                $this->stoppedExtensions[] = $extension;
            }
        }
    }

    /**
     * @psalm-param callable(Context, int): void $job
     */
    public function workOn(callable $job): Result
    {
        $context = new Context();

        foreach ($this->startedExtensions as $extension) {
            $extension->started($context);
        }

        $stop = null;

        for ($jobIndex = 0;; ++$jobIndex) {
            $job($context, $jobIndex);

            foreach ($this->jobDoneExtensions as $extension) {
                $stop = $extension->jobDone($context, $jobIndex);

                if (null !== $stop) {
                    break 2;
                }
            }
        }

        $result = new Result($jobIndex + 1, $stop->reason ?? null);

        foreach ($this->stoppedExtensions as $extension) {
            $extension->stopped($context, $result);
        }

        return $result;
    }
}
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker;

interface WorkerJobDoneExtension
{
    public function jobDone(Context $context, int $jobIndex): ?Stop;
}
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker\SleepInterval;

use HappyInc\Worker\Sleep\SleepInterval;
use PHPUnit\Framework\TestCase;

/**
 * @internal
 * @covers \HappyInc\Worker\Sleep\SleepInterval
 *
 * @small
 */
final class SleepIntervalTest extends TestCase
{
    public function testFromMicroseconds(): void
    {
        $interval = SleepInterval::fromMicroseconds(1);

        $this->assertSame(1, $interval->microseconds);
    }

    public function testFromMilliseconds(): void
    {
        $interval = SleepInterval::fromMilliseconds(1);

        $this->assertSame(1000, $interval->microseconds);
    }

    public function testFromSeconds(): void
    {
        $interval = SleepInterval::fromSeconds(1);

        $this->assertSame(1_000_000, $interval->microseconds);
    }
}
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker;

final class Context
{
    /**
     * @psalm-var array<class-string<ContextData>, ContextData>
     */
    private array $data = [];

    /**
     * @template T of ContextData
     * @psalm-param class-string<T> $class
     * @psalm-return T
     */
    public function dataOf(string $class): ContextData
    {
        /** @psalm-var T */
        return $this->data[$class] ??= new $class();
    }
}
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker;

interface WorkerStartedExtension
{
    public function started(Context $context): void;
}
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker\MemoryLimit;

use HappyInc\Worker\Context;
use HappyInc\Worker\Stop;
use HappyInc\Worker\WorkerJobDoneExtension;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Psr\Log\NullLogger;

final class MemoryLimitExtension implements WorkerJobDoneExtension
{
    private MemoryLimit $limit;

    private LoggerInterface $logger;

    /**
     * @psalm-var LogLevel::*
     */
    private string $logLevel;

    /**
     * @psalm-param LogLevel::* $logLevel
     */
    public function __construct(MemoryLimit $limit, ?LoggerInterface $logger = null, string $logLevel = LogLevel::WARNING)
    {
        $this->limit = $limit;
        $this->logger = $logger ?? new NullLogger();
        $this->logLevel = $logLevel;
    }

    public function jobDone(Context $context, int $jobIndex): ?Stop
    {
        $allocatedMemory = memory_get_usage(true);

        if ($allocatedMemory <= $this->limit->bytes) {
            return null;
        }

        $this->logger->log(
            $this->logLevel,
            'Allocated memory of {allocated} bytes exceeded the worker limit of {limit} bytes.',
            [
                'allocated' => $allocatedMemory,
                'limit' => $this->limit->bytes,
            ]
        );

        return new Stop(sprintf(
            'Allocated memory of %d bytes exceeded the worker limit of %d bytes.',
            $allocatedMemory,
            $this->limit->bytes
        ));
    }
}
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker\Sleep;

use HappyInc\Worker\Context;
use HappyInc\Worker\Stop;
use HappyInc\Worker\WorkerJobDoneExtension;

final class SleepExtension implements WorkerJobDoneExtension
{
    private SleepInterval $interval;

    public function __construct(SleepInterval $interval)
    {
        $this->interval = $interval;
    }

    public function jobDone(Context $context, int $jobIndex): ?Stop
    {
        usleep($this->interval->microseconds);

        return null;
    }
}
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker\ProcessSignal;

use HappyInc\Worker\Context;
use HappyInc\Worker\Stop;
use HappyInc\Worker\WorkerJobDoneExtension;
use HappyInc\Worker\WorkerStartedExtension;

final class ProcessSignalExtension implements WorkerStartedExtension, WorkerJobDoneExtension
{
    /**
     * @psalm-var non-empty-list<int>
     */
    private array $signals;

    /**
     * @psalm-param non-empty-list<int> $signals
     */
    public function __construct(array $signals)
    {
        $this->signals = $signals;
    }

    public function started(Context $context): void
    {
        $data = $context->dataOf(ProcessSignalContextData::class);

        $handler = static function (int $signal) use ($data): void {
            $data->receivedSignal = $signal;
        };

        foreach ($this->signals as $signal) {
            pcntl_signal($signal, $handler);
        }
    }

    public function jobDone(Context $context, int $jobIndex): ?Stop
    {
        pcntl_signal_dispatch();

        $data = $context->dataOf(ProcessSignalContextData::class);

        if (null === $data->receivedSignal) {
            return null;
        }

        return new Stop(sprintf('Process received signal %d.', $data->receivedSignal));
    }
}
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker;

interface WorkerStoppedExtension
{
    public function stopped(Context $context, Result $result): void;
}
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker\MemoryLimit;

use HappyInc\Worker\Context;
use PHPUnit\Framework\TestCase;

/**
 * @internal
 * @covers \HappyInc\Worker\MemoryLimit\MemoryLimitExtension
 *
 * @small
 */
final class MemoryLimitExtensionTest extends TestCase
{
    public function testNotStoppedWhenMemoryLimitNotReached(): void
    {
        $context = new Context();
        $limit = MemoryLimit::fromBytes(memory_get_usage(true) + 1024);
        $extension = new MemoryLimitExtension($limit);

        $stop = $extension->jobDone($context, 0);

        $this->assertNull($stop);
    }

    public function testStoppedWhenMemoryLimitReached(): void
    {
        $context = new Context();
        $limit = MemoryLimit::fromBytes(memory_get_usage() - 1024);
        $extension = new MemoryLimitExtension($limit);

        $stop = $extension->jobDone($context, 0);

        $this->assertNotNull($stop);
    }
}
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker\Signaller;

use HappyInc\Worker\Context;
use HappyInc\Worker\Stop;
use HappyInc\Worker\WorkerJobDoneExtension;
use HappyInc\Worker\WorkerStartedExtension;

final class SignallerExtension implements WorkerStartedExtension, WorkerJobDoneExtension
{
    private string $channel;

    private Signaller $signaller;

    public function __construct(string $channel, ?Signaller $signaller = null)
    {
        $this->channel = $channel;
        $this->signaller = $signaller ?? new FileSignaller();
    }

    public function started(Context $context): void
    {
        $data = $context->dataOf(SignallerContextData::class);
        $data->listener = $this->signaller->createListener($this->channel);
    }

    public function jobDone(Context $context, int $jobIndex): ?Stop
    {
        $data = $context->dataOf(SignallerContextData::class);

        if (($data->listener)()) {
            return new Stop(sprintf('A stop signal was received from channel "%s".', $this->channel));
        }

        return null;
    }
}
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker;

use PHPUnit\Framework\TestCase;

/**
 * @internal
 * @covers \HappyInc\Worker\Worker
 *
 * @small
 */
final class WorkerTest extends TestCase
{
    public function testCallSequence(): void
    {
        $extension = new class() implements WorkerStartedExtension, WorkerJobDoneExtension, WorkerStoppedExtension {
            public array $callsSequence = [];

            public function started(Context $context): void
            {
                $this->callsSequence[] = 'started';
            }

            public function jobDone(Context $context, int $jobIndex): ?Stop
            {
                $this->callsSequence[] = 'jobDone';

                return new Stop();
            }

            public function stopped(Context $context, Result $result): void
            {
                $this->callsSequence[] = 'stopped';
            }
        };

        (new Worker([$extension]))->workOn(static function () use ($extension): void {
            $extension->callsSequence[] = 'job';
        });

        $this->assertSame(['started', 'job', 'jobDone', 'stopped'], $extension->callsSequence);
    }

    /**
     * @psalm-suppress InternalClass
     */
    public function testSameContextPassedAround(): void
    {
        $extension = new class() implements WorkerStartedExtension, WorkerJobDoneExtension, WorkerStoppedExtension {
            public ?Context $context = null;

            public function started(Context $context): void
            {
                $this->context = $context;
            }

            public function jobDone(Context $context, int $jobIndex): ?Stop
            {
                WorkerTest::assertSame($this->context, $context);

                return new Stop();
            }

            public function stopped(Context $context, Result $result): void
            {
                WorkerTest::assertSame($this->context, $context);
            }
        };

        (new Worker([$extension]))->workOn(static function (Context $context) use ($extension): void {
            self::assertSame($extension->context, $context);
        });
    }
}
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker;

/**
 * @psalm-immutable
 */
final class Stop
{
    public ?string $reason;

    public function __construct(?string $reason = null)
    {
        $this->reason = $reason;
    }
}
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker;

use PHPUnit\Framework\TestCase;

/**
 * @internal
 * @covers \HappyInc\Worker\Context
 *
 * @small
 */
final class ContextTest extends TestCase
{
    public function testSameObjectReturned(): void
    {
        $context = new Context();

        $data = $context->dataOf(ContextDataStub::class);
        $data2 = $context->dataOf(ContextDataStub::class);

        $this->assertSame($data, $data2);
    }
}
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker;

final class ContextDataStub implements ContextData
{
    public function __construct()
    {
    }
}
<file_sep><?php

declare(strict_types=1);

require_once __DIR__.'/vendor/autoload.php';

return
    (new PhpCsFixer\Config())
        ->setFinder(
            PhpCsFixer\Finder::create()
                ->in([
                    __DIR__.'/src',
                    __DIR__.'/tests',
                ])
                ->append([
                    __FILE__,
                ])
        )
        ->setRules([
            '@PHP71Migration' => true,
            '@PHP71Migration:risky' => true,
            '@PhpCsFixer' => true,
            '@PhpCsFixer:risky' => true,
            '@PHPUnit60Migration:risky' => true,
            'blank_line_before_statement' => [
                'statements' => [
                    'case',
                    'continue',
                    'declare',
                    'default',
                    'return',
                    'throw',
                    'try',
                ],
            ],
            'date_time_immutable' => true,
            'final_class' => true,
            'fopen_flags' => ['b_mode' => true],
            'no_superfluous_phpdoc_tags' => [
                'allow_mixed' => true,
                'remove_inheritdoc' => true,
            ],
            'ordered_class_elements' => [
                'order' => [
                    'use_trait',
                    'constant_public',
                    'constant_protected',
                    'constant_private',
                    'property_public_static',
                    'property_protected_static',
                    'property_private_static',
                    'property_public',
                    'property_protected',
                    'property_private',
                    'construct',
                    'destruct',
                    'phpunit',
                    'method_public_static',
                    'method_protected_static',
                    'method_private_static',
                    'magic',
                    'method_public',
                    'method_protected',
                    'method_private',
                ],
            ],
            'ordered_imports' => ['imports_order' => ['class', 'function', 'const']],
            'php_unit_size_class' => true,
            'php_unit_strict' => false,
            'php_unit_test_case_static_method_calls' => ['call_type' => 'this'],
            'phpdoc_add_missing_param_annotation' => false,
            'phpdoc_to_comment' => false,
            'phpdoc_types_order' => ['null_adjustment' => 'always_last'],
            'phpdoc_var_without_name' => false,
            'random_api_migration' => false,
            'return_assignment' => false,
            'single_line_comment_style' => ['comment_types' => ['hash']],
            'static_lambda' => true,
        ])
    ;
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker\Signaller;

interface Signaller
{
    public function sendSignal(string $channel): void;

    /**
     * @psalm-return callable(): bool
     */
    public function createListener(string $channel): callable;
}
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker\ProcessSignal;

use HappyInc\Worker\ContextData;

/**
 * @internal
 * @psalm-internal \HappyInc\Worker\ProcessSignal
 */
final class ProcessSignalContextData implements ContextData
{
    public ?int $receivedSignal = null;

    public function __construct()
    {
    }
}
<file_sep>VERBOSITY =

v:
	$(eval VERBOSITY := -v)

vv:
	$(eval VERBOSITY := -vv)

vvv:
	$(eval VERBOSITY := -vvv)

vendor: composer.json
	composer install
	touch vendor

test: vendor
	$(EXEC_PHP) vendor/bin/phpunit $(VERBOSITY)

cs: vendor
	$(EXEC_PHP) vendor/bin/php-cs-fixer fix --allow-risky=yes --dry-run --diff $(VERBOSITY)

fixcs: vendor
	$(EXEC_PHP) vendor/bin/php-cs-fixer fix --allow-risky=yes $(VERBOSITY)

psalm: vendor
	$(EXEC_PHP) vendor/bin/psalm $(file)

composer-check-require: vendor
	$(EXEC_PHP) vendor/bin/composer-require-checker check $(VERBOSITY)

composer-unused: vendor
	composer unused $(VERBOSITY)

composer-validate:
	composer validate $(VERBOSITY)

check: test cs psalm composer-check-require composer-unused composer-validate

.PHONY: v vv vvv test cs fixcs psalm composer-check-require composer-unused composer-validate check
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker\MemoryLimit;

/**
 * @psalm-immutable
 */
final class MemoryLimit
{
    private function __construct(public int $bytes)
    {
    }

    /**
     * @psalm-pure
     */
    public static function fromBytes(int $bytes): self
    {
        if ($bytes <= 0) {
            throw new \InvalidArgumentException(sprintf('Memory limit must be a positive number, got %d bytes.', $bytes));
        }

        return new self($bytes);
    }

    /**
     * @psalm-pure
     */
    public static function fromMegabytes(int $megabytes): self
    {
        if ($megabytes <= 0) {
            throw new \InvalidArgumentException(sprintf('Memory limit must be a positive number, got %d megabytes.', $megabytes));
        }

        return new self($megabytes * 1024 ** 2);
    }

    public static function fromIniMemoryLimit(float $share): self
    {
        if (!($share > 0 && $share <= 1)) {
            throw new \InvalidArgumentException(sprintf('Share must be greater than 0 and less than or equal to 1, got %g.', $share));
        }

        $iniLimit = self::parseMemory(ini_get('memory_limit'));

        if ($iniLimit <= 0) {
            return self::fromMegabytes(100);
        }

        return new self((int) ($iniLimit * $share));
    }

    /**
     * @psalm-pure
     */
    private static function parseMemory(string $memory): int
    {
        if (!preg_match('/^(-?\d+)([tgmk])?$/i', trim($memory), $matches)) {
            throw new \InvalidArgumentException(sprintf('Unknown memory string format "%s".', $memory));
        }

        $value = (int) $matches[1];

        if (!isset($matches[2])) {
            return $value;
        }

        switch (strtolower($matches[2])) {
            case 't':
                $value *= 1024;
                // no break
            case 'g':
                $value *= 1024;
                // no break
            case 'm':
                $value *= 1024;
                // no break
            case 'k':
                $value *= 1024;
        }

        return $value;
    }
}
<file_sep><?php

declare(strict_types=1);

namespace HappyInc\Worker;

/**
 * @psalm-immutable
 */
final class Result
{
    public int $jobsDone;

    public ?string $stoppedReason;

    public function __construct(int $jobsDone, ?string $stoppedReason)
    {
        $this->jobsDone = $jobsDone;
        $this->stoppedReason = $stoppedReason;
    }
}
