Skip to content

Commit b2d7485

Browse files
committed
Provide initial code
1 parent bc1bdfe commit b2d7485

File tree

7 files changed

+265
-1
lines changed

7 files changed

+265
-1
lines changed

composer.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
},
2222
"autoload-dev": {
2323
"psr-4": {
24-
"Vectorial1024\\LaravelBackgroundAsync\\Tests\\": "tests/"
24+
"Vectorial1024\\LaravelProcessAsync\\Tests\\": "tests/"
2525
}
2626
},
2727
"authors": [
@@ -41,5 +41,12 @@
4141
},
4242
"scripts": {
4343
"test": "vendor/bin/phpunit tests"
44+
},
45+
"extra": {
46+
"laravel": {
47+
"providers": [
48+
"Vectorial1024\\LaravelProcessAsync\\BackgroundAsyncServiceProvider"
49+
]
50+
}
4451
}
4552
}

src/AsyncTask.php

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
namespace Vectorial1024\LaravelProcessAsync;
4+
5+
use Closure;
6+
use Illuminate\Support\Facades\Process;
7+
use Laravel\SerializableClosure\SerializableClosure;
8+
9+
/**
10+
* The common handler of an AsyncTask; this can be a closure (will be wrapped inside AsyncTask) or an interface instance.
11+
*/
12+
class AsyncTask
13+
{
14+
/**
15+
* The back to be executed in the background.
16+
* @var SerializableClosure|AsyncTaskInterface
17+
*/
18+
private SerializableClosure|AsyncTaskInterface $theTask;
19+
20+
/**
21+
* Creates an AsyncTask instance.
22+
* @param Closure|AsyncTaskInterface $theTask The task to be executed in the background.
23+
*/
24+
public function __construct(Closure|AsyncTaskInterface $theTask)
25+
{
26+
if ($theTask instanceof Closure) {
27+
// convert to serializable closure first
28+
$theTask = new SerializableClosure($theTask);
29+
}
30+
$this->theTask = $theTask;
31+
}
32+
33+
/**
34+
* Inside an available PHP process, runs this AsyncTask instance.
35+
*
36+
* This should only be called from the runner so that we are really inside an available PHP process.
37+
* @return void
38+
* @see AsyncTaskRunnerCommand
39+
*/
40+
public function run(): void
41+
{
42+
// todo startup configs
43+
44+
// then, execute the task itself
45+
if ($this->theTask instanceof SerializableClosure) {
46+
$innerClosure = $this->theTask->getClosure();
47+
$innerClosure();
48+
unset($innerClosure);
49+
} else {
50+
// must be AsyncTaskInterface
51+
$this->theTask->execute();
52+
}
53+
54+
// todo what if want some "task complete" event handling?
55+
return;
56+
}
57+
58+
/**
59+
* Starts this AsyncTask immediately in the background.
60+
* @return void
61+
*/
62+
public function start(): void
63+
{
64+
// assume unix for now
65+
$serializedTask = $this->toBase64Serial();
66+
Process::start("php artisan async:run $serializedTask 2>&1");
67+
}
68+
69+
/**
70+
* Returns the base64-encoded serialization for this object.
71+
*
72+
* This has the benefit of entirely ignoring potential encoding problems, such as '\0' from private instance variables.
73+
*
74+
* This mechanism might have problems if the task closure is too long, but let's be honest: long closures are best converted to dedicated interface objects.
75+
* @return string The special serialization.
76+
* @see self::fromBase64Serial()
77+
*/
78+
public function toBase64Serial(): string
79+
{
80+
return base64_encode(serialize($this));
81+
}
82+
83+
/**
84+
* Returns the AsyncTask instance represented by the given base64-encoded serial.
85+
* @param string $serial The special serialization.
86+
* @return static|null If the serial is valid, then the reconstructed instance will be returned, else returns null.
87+
* @see self::toBase64Serial()
88+
*/
89+
public static function fromBase64Serial(string $serial): ?static
90+
{
91+
$temp = base64_decode($serial, true);
92+
if ($temp === false) {
93+
// bad data
94+
return null;
95+
}
96+
try {
97+
$temp = unserialize($temp);
98+
// also check that we are unserializing into ourselves
99+
if ($temp instanceof static) {
100+
// correct value type
101+
return $temp;
102+
}
103+
// incorrect value type
104+
return null;
105+
} catch (ErrorException) {
106+
// bad data
107+
return null;
108+
}
109+
}
110+
}

src/AsyncTaskInterface.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Vectorial1024\LaravelProcessAsync;
4+
5+
/**
6+
* An interface to describe the details of a background async task.
7+
*
8+
* This is the recommended way in case your task is not suitable for a serialized closure (e.g. it involves anonymous classes).
9+
*/
10+
interface AsyncTaskInterface
11+
{
12+
/**
13+
* Executes the task when the background async runner is ready to handle this task.
14+
*
15+
* Note: result-checking with the task issuer and exception handling (if needed) must be defined within this method.
16+
* @return void
17+
*/
18+
public function execute(): void;
19+
}

src/AsyncTaskRunnerCommand.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
namespace Vectorial1024\LaravelProcessAsync;
4+
5+
use Illuminate\Console\Command;
6+
7+
/**
8+
* The Artisan command to run AsyncTask. DO NOT USE DIRECTLY!
9+
*/
10+
class AsyncTaskRunnerCommand extends Command
11+
{
12+
/**
13+
* The name and signature of the console command.
14+
*
15+
* @var string
16+
*/
17+
protected $signature = 'async:run {task}';
18+
19+
/**
20+
* The console command description.
21+
*
22+
* @var string
23+
*/
24+
protected $description = 'Runs a background async task (DO NOT USE DIRECTLY)';
25+
26+
/**
27+
* Execute the console command.
28+
*
29+
* @return int
30+
*/
31+
public function handle(): int
32+
{
33+
// first, unpack the task
34+
// (Symfony already safeguards the "task" argument to make it required)
35+
$theTask = $this->argument('task');
36+
$theTask = AsyncTask::fromBase64Serial($theTask);
37+
if ($theTask === null) {
38+
// bad underializing; probably bad data
39+
$this->error("Invalid task details!");
40+
return self::INVALID;
41+
}
42+
43+
// the task type is correct; we can execute it!
44+
/** @var AsyncTask $theTask */
45+
$theTask->run();
46+
return self::SUCCESS;
47+
}
48+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Vectorial1024\LaravelProcessAsync;
4+
5+
use Illuminate\Support\ServiceProvider;
6+
7+
class BackgroundAsyncServiceProvider extends ServiceProvider
8+
{
9+
/**
10+
* Register services.
11+
*/
12+
public function register(): void
13+
{
14+
// pass
15+
}
16+
17+
/**
18+
* Bootstrap services.
19+
*/
20+
public function boot(): void
21+
{
22+
$this->commands([
23+
AsyncTaskRunnerCommand::class,
24+
]);
25+
}
26+
}

tests/AsyncTaskTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Vectorial1024\LaravelProcessAsync\Tests;
4+
5+
class AsyncTaskTest extends BaseTestCase
6+
{
7+
// nothing for now
8+
}

tests/BaseTestCase.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
namespace Vectorial1024\LaravelProcessAsync\Tests;
4+
5+
use Illuminate\Contracts\Auth\Authenticatable;
6+
use Orchestra\Testbench\TestCase;
7+
8+
/**
9+
* Base class for project test cases for some common test config.
10+
*/
11+
class BaseTestCase extends TestCase
12+
{
13+
protected function getPackageProviders($app)
14+
{
15+
// load required package providers for our library to work during testing
16+
return [
17+
\Vectorial1024\LaravelProcessAsync\BackgroundAsyncServiceProvider::class
18+
];
19+
}
20+
21+
// ---
22+
23+
public function call($method, $uri, $parameters = [], $files = [], $server = [], $content = null, $changeHistory = true)
24+
{
25+
// pass
26+
return;
27+
}
28+
29+
public function be(Authenticatable $user, $driver = null)
30+
{
31+
// pass
32+
return;
33+
}
34+
35+
public function seed($class = 'DatabaseSeeder')
36+
{
37+
// pass
38+
return;
39+
}
40+
41+
public function artisan($command, $parameters = [])
42+
{
43+
// pass
44+
return;
45+
}
46+
}

0 commit comments

Comments
 (0)