Runs batch jobs configured in NEON. A job is a processor, an optional source of data and named parameters computed from that data. The library is a Nette DI extension.
composer require lossik/runner| branch | minimum patch version |
|---|---|
| 8.2 | 8.2.35 |
| 8.3 | 8.3.36 |
| 8.4 | 8.4.27 |
| 8.5 | 8.5.12 |
Older patch versions are vulnerable to PHP2Shell (CVE-2026-109618, CVE-2026-109619) and are not supported.
composer.json requires only php: ^8.2, because distributions backport security fixes without bumping the
version number; the CI enforces the patched minimums above. Projects on older PHP stay on 2.0.x.
A source returns rows, a parameter turns the rows into one value per row, a processor gets those values:
<?php
namespace App;
use Lossik\Runner\AbstractParameter;
use Lossik\Runner\AbstractProcessor;
use Lossik\Runner\AbstractSource;
final class UserSource extends AbstractSource
{
public function getSourceData(): array
{
return [['id' => 1, 'name' => 'alice'], ['id' => 2, 'name' => 'bob']];
}
}
final class NameParameter extends AbstractParameter
{
public function getName(): string
{
return 'name';
}
public function getParametersItems(array $sourceData): array
{
$items = [];
foreach ($sourceData as $row) {
$items[$row['id']] = ucfirst($row['name']);
}
return $items;
}
}
final class GreetProcessor extends AbstractProcessor
{
private string $greeting = 'Hello';
public function setGreeting(string $greeting): void
{
$this->greeting = $greeting;
}
public function getParameters(): array
{
return ['name'];
}
public function getParametersClasses(): array
{
return [NameParameter::class];
}
public function process(array $parameters): void
{
foreach ($parameters as $id => $values) {
echo $id, ': ', $this->greeting, ', ', $values['name'], "\n";
}
}
}Register the extension and configure the job:
extensions:
runner: Lossik\Runner\RunnerExtension
runner:
greet:
processor:
class: App\GreetProcessor
setup:
- [setGreeting, [Hi]]
source:
class: App\UserSourceThe extension registers an autowired Lossik\Runner\Runner service; $container is the Nette DI container:
<?php
use Lossik\Runner\Runner;
$runner = $container->getByType(Runner::class);
echo implode(', ', $runner->getProcessors()), "\n"; // greet
$runner->Run('greet');
// 1: Hi, Alice
// 2: Hi, Bob
$runner->RunWidthSourceData('greet', [['id' => 7, 'name' => 'carol']]);
// 7: Hi, CarolThe runner section maps job names to their settings:
| key | description |
|---|---|
processor: {class: …, setup: …} |
the class is created without constructor arguments; setup is a list of calls [method, [arguments]] made on the new object |
processor: {service: …} |
the name of a DI service that implements IProcessor |
source: {class: …, setup: …} |
optional |
include: […] |
files or directories with more jobs, see below; a single path may be given as a string |
A processor defined as a service is set up with its parameters the same way as a processor defined by class:
services:
greeter: App\GreetProcessor
runner:
greetService:
processor:
service: greeter
source:
class: App\UserSourceEach item of include is a file or a directory. A directory contributes its files with the .neon extension
(case-insensitive: jobs.NEON is loaded, jobs.neon.bak is not); the trailing / is optional. A file listed
directly must have the .neon extension too (case-insensitive); other files are skipped. An included file has
the same shape as the runner section, so it may contain jobs and its own include. A path that does not exist is
skipped. A file reached through different paths is loaded only once.
runner:
include:
- %appDir%/config/jobs
greet:
processor:
class: App\GreetProcessor
setup:
- [setGreeting, [Hi]]
source:
class: App\UserSource%appDir%/config/jobs/greet.neon:
greet:
processor:
setup:
- [setGreeting, [Hello]]
farewell:
processor:
class: App\GreetProcessor
setup:
- [setGreeting, [Bye]]
source:
class: App\UserSourceWith these two files the runner has the jobs greet and farewell, and Run('greet') prints 1: Hello, Alice and
2: Hello, Bob:
- When an included file defines a job that also exists in the
runnersection, the values from the included file win (e.g. itsclass), and lists such assetupare concatenated: the calls from therunnersection first, then those from the included file. %parameters%are expanded only in the main configuration file. Use them there to build absolute paths, such as%appDir%/…above; in an included file%name%stays a literal string.- In debug mode the container is rebuilt when an included file changes or a
.neonfile is added to an included directory.
| method | description |
|---|---|
getProcessors() |
names of the configured jobs |
Run(?string $processorName = null) |
runs the job; without a name runs all jobs; an unknown name throws LogicException |
RunWidthSourceData(string $processorName, array $sourceData) |
runs the job with the given rows instead of its source; an unknown name throws LogicException |
registerParameter(IParameter $parameter) |
registers a parameter under its getName() |
The names Run and RunWidthSourceData do not follow PSR-1 and are kept for backward compatibility until the next
major version.
A job runs in these steps:
- The processor is created from
classorservice. - Each class from the processor's
getParametersClasses()is created, passed tosetupParameter()and registered. - The rows are read from the source (or taken from
RunWidthSourceData()). - For each name from
getParameters(), the registered parameter'sgetParametersItems($rows)returns values by row key; the processor'sprocess()gets them as[rowKey => [parameterName => value, …], …].
Parameters are registered in the Runner instance, so a job may use a parameter registered by a job run before it.
A name from getParameters() that is not registered throws LogicException.
| interface | base class | methods |
|---|---|---|
Lossik\Runner\IProcessor |
AbstractProcessor |
getParameters() (parameter names), getParametersClasses() (parameter classes to create), setupParameter(IParameter $parameter), process(array $parameters) |
Lossik\Runner\IParameter |
AbstractParameter |
getName(), getParametersItems(array $sourceData) (values by row key) |
Lossik\Runner\ISource |
AbstractSource |
getSourceData() (the rows) |
The Abstract* base classes implement the interface with default methods, so a subclass overrides only what it
needs.
- A relative path in
includeis resolved against the current working directory of the process, not against the configuration file. Build absolute paths with a parameter in the main configuration file (see above). - A class from
class:that does not implement the required interface (IProcessor,IParameter,ISource) is reported byLogicExceptionwith an empty message. - An
includepath that does not exist is skipped; when it is created after the container was compiled, the container is not rebuilt in debug mode. Run('')andRun('0')run all jobs, likeRun().- A
processorwithoutclassand withoutserviceends withTypeError.
MIT, see LICENSE.