1
0
Fork 0
mirror of https://github.com/archtechx/jobpipeline.git synced 2025-12-12 09:24:02 +00:00

Initial commit

This commit is contained in:
Samuel Štancl 2020-05-15 18:07:28 +02:00
commit 90c59626c2
8 changed files with 5460 additions and 0 deletions

20
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,20 @@
name: CI
on: [ push, pull_request ]
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
laravel: ["^6.0", "^7.0"]
steps:
- uses: actions/checkout@v2
- name: Install dependencies
run: composer install
- name: Start Redis
uses: supercharge/redis-github-action@1.1.0
- name: Run tests
run: phpunit

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/vendor/

29
composer.json Normal file
View file

@ -0,0 +1,29 @@
{
"name": "stancl/jobpipeline",
"description": "Turn any series of jobs into Laravel listeners.",
"license": "MIT",
"authors": [
{
"name": "Samuel Štancl",
"email": "samuel.stancl@gmail.com"
}
],
"autoload": {
"psr-4": {
"Stancl\\JobPipeline\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Stancl\\JobPipeline\\Tests\\": "tests/"
}
},
"require": {
"illuminate/support": "^7.11"
},
"require-dev": {
"orchestra/testbench": "^5.2",
"spatie/valuestore": "^1.2",
"ext-redis": "*"
}
}

5085
composer.lock generated Normal file

File diff suppressed because it is too large Load diff

0
docker-compose.yml Normal file
View file

35
phpunit.xml Normal file
View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./src</directory>
<exclude>
<file>./src/routes.php</file>
</exclude>
</whitelist>
</filter>
<php>
<env name="APP_ENV" value="testing"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_DRIVER" value="redis"/>
<env name="MAIL_DRIVER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="AWS_DEFAULT_REGION" value="us-west-2"/>
</php>
</phpunit>

95
src/JobPipeline.php Normal file
View file

@ -0,0 +1,95 @@
<?php
namespace Stancl\JobPipeline;
use Closure;
use Illuminate\Contracts\Queue\ShouldQueue;
class JobPipeline implements ShouldQueue
{
/** @var bool */
public static $shouldBeQueuedByDefault = false;
/** @var callable[]|string[] */
public $jobs;
/** @var callable|null */
public $send;
/**
* A value passed to the jobs. This is the return value of $send.
*/
public $passable;
/** @var bool */
public $shouldBeQueued;
public function __construct($jobs, callable $send = null, bool $shouldBeQueued = null)
{
$this->jobs = $jobs;
$this->send = $send ?? function ($event) {
// If no $send callback is set, we'll just pass the event through the jobs.
return $event;
};
$this->shouldBeQueued = $shouldBeQueued ?? static::$shouldBeQueuedByDefault;
}
/** @param callable[]|string[] $jobs */
public static function make(array $jobs): self
{
return new static($jobs);
}
public function send(callable $send): self
{
$this->send = $send;
return $this;
}
public function shouldBeQueued(bool $shouldBeQueued)
{
$this->shouldBeQueued = $shouldBeQueued;
return $this;
}
public function handle(): void
{
foreach ($this->jobs as $job) {
app()->call([new $job(...$this->passable), 'handle']);
}
}
/**
* Generate a closure that can be used as a listener.
*/
public function toListener(): Closure
{
return function (...$args) {
$executable = $this->executable($args);
if ($this->shouldBeQueued) {
dispatch($executable);
} else {
dispatch_now($executable);
}
};
}
/**
* Return a serializable version of the current object.
*/
public function executable($listenerArgs): self
{
$clone = clone $this;
$passable = ($clone->send)(...$listenerArgs);
$passable = is_array($passable) ? $passable : [$passable];
$clone->passable = $passable;
unset($clone->send);
return $clone;
}
}

195
tests/JobPipelineTest.php Normal file
View file

@ -0,0 +1,195 @@
<?php
namespace Stancl\JobPipeline\Tests;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Queue;
use Orchestra\Testbench\TestCase;
use Spatie\Valuestore\Valuestore;
use Stancl\JobPipeline\JobPipeline;
class JobPipelineTest extends TestCase
{
public $mockConsoleOutput = false;
/** @var Valuestore */
protected $valuestore;
public function setUp(): void
{
parent::setUp();
config(['queue.default' => 'redis']);
$this->valuestore = Valuestore::make(__DIR__ . '/tmp/jobpipelinetest.json')->flush();
}
/** @test */
public function job_pipeline_can_listen_to_any_event()
{
Event::listen(TestEvent::class, JobPipeline::make([
FooJob::class,
])->send(function () {
return $this->valuestore;
})->toListener());
$this->assertFalse($this->valuestore->has('foo'));
event(new TestEvent(new TestModel()));
$this->assertSame('bar', $this->valuestore->get('foo'));
}
/** @test */
public function job_pipeline_can_be_queued()
{
Queue::fake();
Event::listen(TestEvent::class, JobPipeline::make([
FooJob::class,
])->send(function () {
return $this->valuestore;
})->shouldBeQueued(true)->toListener());
Queue::assertNothingPushed();
event(new TestEvent(new TestModel()));
$this->assertFalse($this->valuestore->has('foo'));
Queue::pushed(JobPipeline::class, function (JobPipeline $pipeline) {
$this->assertSame([FooJob::class], $pipeline->jobs);
});
}
/** @test */
public function job_pipelines_run_when_queued()
{
Event::listen(TestEvent::class, JobPipeline::make([
FooJob::class,
])->send(function () {
return $this->valuestore;
})->shouldBeQueued(true)->toListener());
$this->assertFalse($this->valuestore->has('foo'));
event(new TestEvent(new TestModel()));
$this->artisan('queue:work --once');
$this->assertSame('bar', $this->valuestore->get('foo'));
}
/** @test */
public function job_pipeline_executes_jobs_and_passes_the_object_sequentially()
{
Event::listen(TestEvent::class, JobPipeline::make([
FirstJob::class,
SecondJob::class,
])->send(function (TestEvent $event) {
return [$event->testModel, $this->valuestore];
})->toListener());
$this->assertFalse($this->valuestore->has('foo'));
event(new TestEvent(new TestModel()));
$this->assertSame('first job changed property', $this->valuestore->get('foo'));
}
/** @test */
public function send_can_return_multiple_arguments()
{
Event::listen(TestEvent::class, JobPipeline::make([
JobWithMultipleArguments::class
])->send(function () {
return ['a', 'b'];
})->toListener());
$this->assertFalse(app()->bound('test_args'));
event(new TestEvent(new TestModel()));
$this->assertSame(['a', 'b'], app('test_args'));
}
}
class FooJob
{
protected $valuestore;
public function __construct(Valuestore $valuestore)
{
$this->valuestore = $valuestore;
}
public function handle()
{
$this->valuestore->put('foo', 'bar');
}
};
class TestModel extends Model
{
}
class TestEvent
{
/** @var TestModel $testModel */
public $testModel;
public function __construct(TestModel $testModel)
{
$this->testModel = $testModel;
}
}
class FirstJob
{
public $testModel;
public function __construct(TestModel $testModel)
{
$this->testModel = $testModel;
}
public function handle()
{
$this->testModel->foo = 'first job changed property';
}
}
class SecondJob
{
public $testModel;
protected $valuestore;
public function __construct(TestModel $testModel, Valuestore $valuestore)
{
$this->testModel = $testModel;
$this->valuestore = $valuestore;
}
public function handle()
{
$this->valuestore->put('foo', $this->testModel->foo);
}
}
class JobWithMultipleArguments
{
protected $first;
protected $second;
public function __construct($first, $second)
{
$this->first = $first;
$this->second = $second;
}
public function handle()
{
// we dont queue this job so no need to use valuestore here
app()->instance('test_args', [$this->first, $this->second]);
}
}