diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..3ba6281
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,2 @@
+github: MoamenEltouny
+buy_me_a_coffee: moameneltouny
\ No newline at end of file
diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml
new file mode 100644
index 0000000..5a596f1
--- /dev/null
+++ b/.github/workflows/php-cs-fixer.yml
@@ -0,0 +1,23 @@
+name: PHP Coding Standards Fixer
+
+on: [push]
+
+jobs:
+ php-cs-fixer:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.head_ref }}
+
+ - name: Run PHP CS Fixer
+ uses: docker://oskarstark/php-cs-fixer-ga
+ with:
+ args: --config=.php-cs-fixer.dist.php --allow-risky=yes
+
+ - name: Commit changes
+ uses: stefanzweifel/git-auto-commit-action@v4
+ with:
+ commit_message: Fixing php-cs
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/tests.yml
similarity index 74%
rename from .github/workflows/build.yml
rename to .github/workflows/tests.yml
index 5a014ea..789cb38 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/tests.yml
@@ -1,6 +1,9 @@
-name: build
+name: Tests
-on: [push, pull_request]
+on:
+ push:
+ branches: [ main, develop ]
+ pull_request:
jobs:
run:
@@ -8,7 +11,7 @@ jobs:
strategy:
matrix:
operating-system: [ubuntu-latest]
- php-versions: ["8.1", "8.2", "8.3"]
+ php-versions: ['8.2', '8.3', '8.4']
name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }}
steps:
@@ -19,7 +22,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
- extensions: mbstring, pdo, pdo_sqlite, sqlite3, intl, zip
+ extensions: mbstring, pdo, intl, zip
coverage: none
- name: Check PHP Version
@@ -35,7 +38,7 @@ jobs:
run: composer validate
- name: Install dependencies
- run: composer install --prefer-dist --no-progress --no-suggest
+ run: composer install --prefer-dist --no-progress
- name: Run test suite
- run: vendor/bin/phpunit
+ run: vendor/bin/phpunit --testdox
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 35024a0..929319b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
-vendor
+/vendor
composer.lock
-.phpunit.cache
\ No newline at end of file
+.phpunit.result.cache
+.php-cs-fixer.cache
\ No newline at end of file
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
new file mode 100644
index 0000000..70e2ea4
--- /dev/null
+++ b/.php-cs-fixer.dist.php
@@ -0,0 +1,39 @@
+notPath('bootstrap/*')
+ ->notPath('storage/*')
+ ->notPath('storage/*')
+ ->notPath('resources/view/mail/*')
+ ->in([
+ __DIR__ . '/src',
+ __DIR__ . '/tests',
+ ])
+ ->name('*.php')
+ ->notName('*.blade.php')
+ ->ignoreDotFiles(true)
+ ->ignoreVCS(true);
+
+return (new PhpCsFixer\Config())
+ ->setRules([
+ '@PSR12' => true,
+ 'indentation_type' => true,
+ 'array_syntax' => ['syntax' => 'short'],
+ 'ordered_imports' => ['sort_algorithm' => 'alpha'],
+ 'no_unused_imports' => true,
+ 'not_operator_with_successor_space' => true,
+ 'trailing_comma_in_multiline' => true,
+ 'phpdoc_scalar' => true,
+ 'unary_operator_spaces' => true,
+ 'binary_operator_spaces' => true,
+ 'blank_line_before_statement' => [
+ 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'],
+ ],
+ 'phpdoc_single_line_var_spacing' => true,
+ 'phpdoc_var_without_name' => true,
+ 'method_argument_space' => [
+ 'on_multiline' => 'ensure_fully_multiline',
+ 'keep_multiple_spaces_after_comma' => true,
+ ],
+ ])
+ ->setFinder($finder);
diff --git a/README.md b/README.md
index 9bbdbe8..db0108c 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,11 @@

-
-
-
-
-
-
-
+
+
+
+
+
Allows you to execute commands, actions, jobs, and automated tasks on your production server.
diff --git a/composer.json b/composer.json
index 79f4465..1a26986 100644
--- a/composer.json
+++ b/composer.json
@@ -24,11 +24,12 @@
}
],
"require": {
- "php": ">=8.1",
- "laravel/framework": ">=10.0"
+ "php": "~8.2|~8.3|~8.4",
+ "laravel/framework": "^11.0",
+ "pharaonic/php-dot-array": "^2.0"
},
"require-dev": {
- "orchestra/testbench": "^8.0"
+ "orchestra/testbench": "^9.15"
},
"config": {
"sort-packages": true
diff --git a/config/executor.php b/config/executor.php
new file mode 100644
index 0000000..f8defa3
--- /dev/null
+++ b/config/executor.php
@@ -0,0 +1,13 @@
+ env('EXECUTOR_CONNECTION', config('database.default')),
+
+ /**
+ * The table that should be used to store the executors.
+ */
+ 'table' => env('EXECUTOR_TABLE', 'executors'),
+];
diff --git a/database/migrations/2024_07_07_000001_create_executors_table.php b/database/migrations/2024_07_07_000001_create_executors_table.php
index 23ad719..ee4b7a9 100644
--- a/database/migrations/2024_07_07_000001_create_executors_table.php
+++ b/database/migrations/2024_07_07_000001_create_executors_table.php
@@ -14,15 +14,17 @@ class CreateExecutorsTable extends Migration
*/
public function up()
{
- Schema::create('executors', function (Blueprint $table) {
- $table->id();
- $table->unsignedTinyInteger('type')->default(ExecutorType::Always);
- $table->string('executor');
- $table->string('tag')->nullable();
- $table->integer('batch')->default(1);
- $table->integer('executed')->default(0);
- $table->timestamp('last_executed_at')->nullable();
- });
+ Schema::connection(config('pharaonic.executor.connection', config('database.default')))
+ ->create(config('pharaonic.executor.table', 'executors'), function (Blueprint $table) {
+ $table->id();
+ $table->unsignedTinyInteger('type')->default(ExecutorType::Always);
+ $table->string('name');
+ $table->json('tags')->nullable();
+ $table->json('servers')->nullable();
+ $table->integer('batch')->nullable();
+ $table->integer('executed')->default(0);
+ $table->timestamp('last_executed_at')->nullable();
+ });
}
/**
@@ -32,6 +34,7 @@ public function up()
*/
public function down()
{
- Schema::dropIfExists('executors');
+ Schema::connection(config('pharaonic.executor.connection', config('database.default')))
+ ->dropIfExists(config('pharaonic.executor.table', 'executors'));
}
}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 5d2e220..84e9176 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,13 +1,13 @@
-
-
-
- ./tests/
-
-
-
-
- ./src/
-
-
+
+
+
+
+ tests
+
+
+
diff --git a/src/Executor.php b/src/Classes/Executor.php
similarity index 60%
rename from src/Executor.php
rename to src/Classes/Executor.php
index b504264..bfa00f4 100644
--- a/src/Executor.php
+++ b/src/Classes/Executor.php
@@ -1,15 +1,24 @@
type instanceof ExecutorType)) {
+ if (! ($this->type instanceof ExecutorType)) {
throw new \Exception('The type of the executor must be an instance of ExecutorType.');
}
- if (!is_null($this->tag) && !is_string($this->tag) && !is_array($this->tag)) {
- throw new \Exception('The tag of the executor must be a string or an array or null.');
+ if (! is_array($this->tags)) {
+ throw new \Exception('The tags of the executor must be an array.');
+ }
+
+ if (! is_array($this->servers)) {
+ throw new \Exception('The servers of the executor must be an array of ips.');
}
$this->output = new OutputStyle(new ArgvInput(), new ConsoleOutput());
diff --git a/src/Classes/ExecutorItem.php b/src/Classes/ExecutorItem.php
new file mode 100644
index 0000000..6a4930d
--- /dev/null
+++ b/src/Classes/ExecutorItem.php
@@ -0,0 +1,128 @@
+executor = $executor;
+ $this->file = $file;
+ $this->name = $name;
+ $this->model = $model;
+ }
+
+ /**
+ * Get info about the executor.
+ *
+ * @return array
+ */
+ public function info()
+ {
+ return [
+ 'name' => $this->name,
+ 'path' => $this->file->getRealPath(),
+ 'type' => $this->executor->type,
+ 'tags' => $this->executor->tags ?: null,
+ 'servers' => $this->executor->servers ?: null,
+ 'batch' => $this->model?->batch,
+ 'executed' => $this->model?->executed > 0,
+ ];
+ }
+
+ /**
+ * Determine if the executor is executable.
+ *
+ * @return bool
+ */
+ public function isExecutable()
+ {
+ if (! $this->model) {
+ return true;
+ }
+
+ if ($servers = $this->executor->servers ?: null) {
+ if (! array_intersect($servers, ExecutorFacade::getIPs())) {
+ return false;
+ }
+ }
+
+ $this->model?->fill([
+ 'type' => $this->executor->type,
+ 'tags' => $this->executor->tags,
+ 'servers' => $this->executor->servers,
+ ]);
+
+ return $this->model?->isExecutable() ?? true;
+ }
+
+ /**
+ * Run the executor.
+ *
+ * @return void
+ */
+ public function run(int $nextBatch)
+ {
+ $this->executor->up();
+
+ if (! $this->model) {
+ $this->model = Model::create([
+ 'type' => $this->executor->type,
+ 'name' => $this->name,
+ 'tags' => $this->executor->tags,
+ 'batch' => $nextBatch,
+ 'executed' => 1,
+ 'last_executed_at' => now(),
+ ]);
+ } else {
+ $this->model->executed += 1;
+ $this->model->last_executed_at = now();
+ $this->model->save();
+ }
+ }
+
+ /**
+ * Rollback the executor.
+ *
+ * @return void
+ */
+ public function rollback()
+ {
+ $this->executor->down();
+ }
+}
diff --git a/src/Classes/ExecutorManager.php b/src/Classes/ExecutorManager.php
new file mode 100644
index 0000000..5a6627f
--- /dev/null
+++ b/src/Classes/ExecutorManager.php
@@ -0,0 +1,86 @@
+pool = new ExecutorPool();
+ }
+
+ /**
+ * Get the executor pool instance.
+ *
+ * @return ExecutorPool
+ */
+ public function getPool()
+ {
+ return $this->pool;
+ }
+
+ /**
+ * Get all executor records.
+ *
+ * @return \Illuminate\Support\Collection
+ */
+ public function getRecords()
+ {
+ return Executor::all()->keyBy('name');
+ }
+
+ /**
+ * Get info about all executors.
+ *
+ * @return array
+ */
+ public function info()
+ {
+ return $this->pool
+ ->collect($this->getRecords())
+ ->info();
+ }
+
+ /**
+ * Get the next batch number.
+ *
+ * @return int
+ */
+ public function getNextBatchNumber()
+ {
+ return (Executor::orderBy('batch', 'desc')->first()?->batch ?? 0) + 1;
+ }
+
+ /**
+ * Get the server IPs.
+ *
+ * @return array
+ */
+ public function getIPs()
+ {
+ if (! empty($this->ips)) {
+ return $this->ips;
+ }
+
+ return $this->ips = array_filter(
+ dot(net_get_interfaces())->get('*.unicast.*.address'),
+ fn ($ip) => ! in_array($ip, ['127.0.0.1', '::1', null])
+ );
+ }
+}
diff --git a/src/Classes/ExecutorPool.php b/src/Classes/ExecutorPool.php
new file mode 100644
index 0000000..d9d06da
--- /dev/null
+++ b/src/Classes/ExecutorPool.php
@@ -0,0 +1,111 @@
+paths = [base_path('executors')];
+ }
+
+ /**
+ * Add a new path to executors pools.
+ *
+ * @param string $path
+ * @return static
+ */
+ public function addPath(string $path)
+ {
+ array_push($this->paths, $path);
+
+ return $this;
+ }
+
+ /**
+ * Return all pools of executors.
+ *
+ * @return array
+ */
+ public function getPaths(): array
+ {
+ return $this->paths;
+ }
+
+ /**
+ * Get info about all executors.
+ *
+ * @return array
+ */
+ public function info()
+ {
+ return collect($this->items)->map(function ($item) {
+ return $item->info();
+ })->toArray();
+ }
+
+ /**
+ * Collect all executors from the defined paths.
+ *
+ * @param Collection $records
+ * @return static
+ */
+ public function collect(Collection $records)
+ {
+ if (! empty($this->items)) {
+ $this->items = [];
+ }
+
+ foreach ($this->paths as $path) {
+ if (File::isDirectory($path) && ! File::isEmptyDirectory($path, true)) {
+ foreach (File::files($path) as $file) {
+ if ($file->getExtension() === 'php') {
+ $obj = include $file->getRealPath();
+
+ if (! $obj instanceof Executor) {
+ continue;
+ }
+
+ $name = basename($file->getFileName(), '.php');
+ $record = $records->get($name);
+
+ if (isset($this->items[$name])) {
+ throw new \Exception("Duplicate Executor name [$name] in file: " . $file->getRealPath());
+ }
+
+ $this->items[$name] = new ExecutorItem($obj, $file, $name, $record);
+ }
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get all collected executors items.
+ *
+ * @return array
+ */
+ public function getItems()
+ {
+ return $this->items;
+ }
+}
diff --git a/src/Classes/ExecutorPoolClass.php b/src/Classes/ExecutorPoolClass.php
deleted file mode 100644
index d390051..0000000
--- a/src/Classes/ExecutorPoolClass.php
+++ /dev/null
@@ -1,39 +0,0 @@
-paths = [base_path('executors')];
- }
-
- /**
- * Return all pools of executors.
- *
- * @return array
- */
- public function getPaths(): array
- {
- return $this->paths;
- }
-
- /**
- * Add a new path to executors pools.
- *
- * @param string $path
- * @return void
- */
- public function addPath(string $path): void
- {
- array_push($this->paths, $path);
- }
-}
diff --git a/src/Console/ExecuteCommand.php b/src/Console/ExecuteCommand.php
index b8db1e5..abdf8f1 100644
--- a/src/Console/ExecuteCommand.php
+++ b/src/Console/ExecuteCommand.php
@@ -3,7 +3,7 @@
namespace Pharaonic\Laravel\Executor\Console;
use Illuminate\Console\Command;
-use Pharaonic\Laravel\Executor\Services\ExecutorService;
+use Pharaonic\Laravel\Executor\Facades\Executor as ExecutorFacade;
class ExecuteCommand extends Command
{
@@ -13,7 +13,7 @@ class ExecuteCommand extends Command
* @var string
*/
protected $signature = 'execute {name?}
- {--tag= : Execute the executor with the specified tag}';
+ {--tags= : Execute the executor with the specified tags}';
/**
* The console command description.
@@ -25,31 +25,41 @@ class ExecuteCommand extends Command
/**
* Execute the console command.
*/
- public function handle(ExecutorService $service)
+ public function handle()
{
- if (!$service->isExists()) {
- $this->warn('There are no executors need to be executed.');
- }
+ $items = ExecutorFacade::getPool()
+ ->collect(ExecutorFacade::getRecords())
+ ->getItems();
- $list = $service->sync();
$name = $this->argument('name');
- $tag = $this->option('tag');
+ $tags = $this->option('tags');
+ $toRun = [];
- $executors = $list
- ->filter(fn ($executor) => $executor['model']->executable)
- ->when($name, fn ($executors) => $executors->where('name', $name))
- ->when($tag, fn ($executors) => $executors->where('tag', $tag));
+ foreach ($items as $item) {
+ if ($name && $item->name != $name) {
+ continue;
+ }
- if ($executors->isEmpty()) {
- $this->warn('There are no executors need to be executed.');
+ if ($tags && ! array_intersect(explode(',', $tags), $item->tags)) {
+ continue;
+ }
+
+ if ($item->isExecutable()) {
+ $toRun[] = $item;
+ }
}
- $executors->each(function ($executor) {
- $this->info("Executing {$executor['name']}...");
+ if (empty($toRun)) {
+ $this->warn('There are no executors need to be executed.');
+ } else {
+ $batch = ExecutorFacade::getNextBatchNumber();
+
+ foreach ($toRun as $item) {
+ $this->info("Executing {$item->name}...");
- (include $executor['path'])->handle();
- $executor['model']->execute();
- });
+ $item->run($batch);
+ }
+ }
return 0;
}
diff --git a/src/Console/ExecuteFreshCommand.php b/src/Console/ExecuteFreshCommand.php
index a5f3f88..6cf9909 100644
--- a/src/Console/ExecuteFreshCommand.php
+++ b/src/Console/ExecuteFreshCommand.php
@@ -23,7 +23,7 @@ class ExecuteFreshCommand extends Command
/**
* Execute the console command.
- *
+ *
* @return int
*/
public function handle()
diff --git a/src/Console/ExecuteMakeCommand.php b/src/Console/ExecuteMakeCommand.php
index c0194ad..b6c462d 100644
--- a/src/Console/ExecuteMakeCommand.php
+++ b/src/Console/ExecuteMakeCommand.php
@@ -14,8 +14,6 @@ class ExecuteMakeCommand extends GeneratorCommand
* @var string
*/
protected $signature = 'execute:make {name}
- {--o|once : Create a new executor class that will be executed once}
- {--tag= : The tag of the executor}
{--path= : The path of the executor}';
/**
@@ -42,81 +40,6 @@ protected function getStub()
return __DIR__ . '/../../stubs/executor.php.stub';
}
- /**
- * Replace the class name for the given stub.
- *
- * @param string $stub
- * @param string $name
- * @return string
- */
- protected function replaceClass($stub, $name)
- {
- $stub = parent::replaceClass($stub, $name);
-
- $this->setTypeProperty($stub);
- $this->setTagProperty($stub);
-
- return $stub;
- }
-
- /**
- * Set the type property for the executor.
- *
- * @param string $stub
- * @return void
- */
- protected function setTypeProperty(string &$stub)
- {
- if (!$this->option('once')) {
- $stub = str_replace(
- [
- "{{ type }}\n",
- "{{ type-use }}\n"
- ],
- '',
- $stub
- );
-
- return;
- }
-
- $stub = str_replace(
- [
- '{{ type-use }}',
- '{{ type }}',
- ],
- [
- 'use Pharaonic\Laravel\Executor\Enums\ExecutorType;',
- file_get_contents(__DIR__ . '/../../stubs/property/once.stub') . PHP_EOL,
- ],
- $stub
- );
- }
-
- /**
- * Replace the once option for the given stub.
- *
- * @param string $stub
- * @return void
- */
- protected function setTagProperty(string &$stub)
- {
- if (!$this->option('tag')) {
- $stub = str_replace("{{ tag }}\n", '', $stub);
- return;
- }
-
- $stub = str_replace(
- '{{ tag }}',
- str_replace(
- '{{ tag }}',
- '"' . $this->option('tag') . '"',
- file_get_contents(__DIR__ . '/../../stubs/property/tag.stub')
- ) . PHP_EOL,
- $stub
- );
- }
-
/**
* Get the destination class path.
*
diff --git a/src/Console/ExecuteRollbackCommand.php b/src/Console/ExecuteRollbackCommand.php
index 6c0a55d..bac453d 100644
--- a/src/Console/ExecuteRollbackCommand.php
+++ b/src/Console/ExecuteRollbackCommand.php
@@ -3,6 +3,7 @@
namespace Pharaonic\Laravel\Executor\Console;
use Illuminate\Console\Command;
+use Pharaonic\Laravel\Executor\Facades\Executor as ExecutorFacade;
use Pharaonic\Laravel\Executor\Models\Executor;
class ExecuteRollbackCommand extends Command
@@ -20,11 +21,11 @@ class ExecuteRollbackCommand extends Command
*
* @var string
*/
- protected $description = 'Rollback the lastest executors that has been inserted.';
+ protected $description = 'Rollback the latest executors that have been inserted.';
/**
* Execute the console command.
- *
+ *
* @return int
*/
public function handle()
@@ -32,13 +33,26 @@ public function handle()
$batches = Executor::orderBy('batch', 'desc')->groupBy('batch')->limit($this->option('steps'))->pluck('batch')->toArray();
if (empty($batches)) {
- $this->error('There are no executors has been found.');
- return 1;
+ $this->warn('There are no executors has been found.');
+
+ return 0;
+ }
+
+ $items = ExecutorFacade::getPool()
+ ->collect(ExecutorFacade::getRecords())
+ ->getItems();
+
+ foreach ($items as $item) {
+ if ($item->model && in_array($item->model->batch, $batches)) {
+ $this->info("Rolling back {$item->name}...");
+
+ $item->rollback();
+ }
}
Executor::whereIn('batch', $batches)->delete();
- $this->info('Executors has been rollbacked successfully.');
+ $this->info('Executors has been rollback successfully.');
return 0;
}
diff --git a/src/Console/ExecuteStatusCommand.php b/src/Console/ExecuteStatusCommand.php
index cb34e70..b16106d 100644
--- a/src/Console/ExecuteStatusCommand.php
+++ b/src/Console/ExecuteStatusCommand.php
@@ -3,7 +3,7 @@
namespace Pharaonic\Laravel\Executor\Console;
use Illuminate\Console\Command;
-use Pharaonic\Laravel\Executor\Services\ExecutorService;
+use Pharaonic\Laravel\Executor\Facades\Executor;
class ExecuteStatusCommand extends Command
{
@@ -24,19 +24,18 @@ class ExecuteStatusCommand extends Command
/**
* Execute the console command.
*/
- public function handle(ExecutorService $service)
+ public function handle()
{
- $executors = $service->sync();
-
$this->table(
- ['Name', 'Type', 'Tag', 'Batch', 'Executed'],
- $executors->map(function ($executor) {
+ ['Name', 'Type', 'Tags', 'Servers', 'Batch', 'Executed'],
+ collect(Executor::info())->map(function ($executor) {
return [
$executor['name'],
- ucfirst($executor['type']->name),
- $executor['tag'],
- $executor['model']->batch,
- $executor['model']->executed > 0 ? 'Yes' : 'No',
+ $executor['type']->name,
+ empty($executor['tags']) ? 'None' : implode(', ', $executor['tags']),
+ empty($executor['servers']) ? 'All' : implode(', ', $executor['servers']),
+ $executor['batch'] ?? 'N/A',
+ $executor['executed'] ? 'Yes' : 'No',
];
})
);
diff --git a/src/Enums/ExecutorType.php b/src/Enums/ExecutorType.php
index 2ee201f..bdd0db1 100644
--- a/src/Enums/ExecutorType.php
+++ b/src/Enums/ExecutorType.php
@@ -14,7 +14,7 @@ enum ExecutorType: int
/**
* Check if the executor will be executed always.
*
- * @return boolean
+ * @return bool
*/
public function isAlways(): bool
{
@@ -24,7 +24,7 @@ public function isAlways(): bool
/**
* Check if the executor will be executed once.
*
- * @return boolean
+ * @return bool
*/
public function isOnce(): bool
{
diff --git a/src/ExecutorServiceProvider.php b/src/ExecutorServiceProvider.php
index efba8c2..492ef78 100644
--- a/src/ExecutorServiceProvider.php
+++ b/src/ExecutorServiceProvider.php
@@ -2,8 +2,9 @@
namespace Pharaonic\Laravel\Executor;
+use Illuminate\Foundation\Console\AboutCommand;
use Illuminate\Support\ServiceProvider;
-use Pharaonic\Laravel\Executor\Classes\ExecutorPoolClass;
+use Pharaonic\Laravel\Executor\Classes\ExecutorManager;
use Pharaonic\Laravel\Executor\Console\ExecuteCommand;
use Pharaonic\Laravel\Executor\Console\ExecuteFreshCommand;
use Pharaonic\Laravel\Executor\Console\ExecuteMakeCommand;
@@ -19,7 +20,10 @@ class ExecutorServiceProvider extends ServiceProvider
*/
public function register()
{
- $this->app->singleton('pharaonic.executor.executorPool', ExecutorPoolClass::class);
+ $this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
+ $this->mergeConfigFrom(__DIR__ . '/../config/executor.php', 'pharaonic.executor');
+
+ $this->app->singleton('pharaonic.executor.manager', ExecutorManager::class);
}
/**
@@ -30,16 +34,18 @@ public function register()
public function boot()
{
if ($this->app->runningInConsole()) {
- // Publish Migrations
+ AboutCommand::add('Pharaonic', ['Executor' => '11.0.0']);
+
$this->publishes(
[__DIR__ . '/../database/migrations' => database_path('migrations')],
- ['pharaonic', 'migrations', 'executor-migrations']
+ ['pharaonic', 'migrations', 'laravel-executor']
);
- // Load Migrations
- $this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
+ $this->publishes(
+ [__DIR__ . '/../config/executor.php' => config_path('pharaonic/executor.php')],
+ ['pharaonic', 'config', 'laravel-executor']
+ );
- // Load Commands
$this->commands([
ExecuteCommand::class,
ExecuteMakeCommand::class,
diff --git a/src/Facades/Executor.php b/src/Facades/Executor.php
new file mode 100644
index 0000000..0a27749
--- /dev/null
+++ b/src/Facades/Executor.php
@@ -0,0 +1,20 @@
+ ExecutorType::class,
+ 'tags' => 'array',
'batch' => 'integer',
'executed' => 'integer',
'last_executed_at' => 'datetime',
];
/**
- * Get the executable attribute.
+ * Get the current connection name for the model.
*
- * @return void
+ * @return string|null
+ */
+ public function getConnectionName()
+ {
+ return config('pharaonic.executor.connection', parent::getConnectionName());
+ }
+
+ /**
+ * Get the table associated with the model.
+ *
+ * @return string
+ */
+ public function getTable()
+ {
+ return config('pharaonic.executor.table', parent::getTable());
+ }
+
+ /**
+ * Check if the executor is new (never executed).
+ *
+ * @return bool
+ */
+ public function isNew()
+ {
+ return $this->executed === 0;
+ }
+
+ /**
+ * Check if the executor is executable.
+ *
+ * @return bool
*/
- public function getExecutableAttribute()
+ public function isExecutable()
{
- return $this->type->isAlways() || ($this->type->isOnce() && $this->executed == 0);
+ return $this->type->isAlways() || $this->isNew();
}
/**
diff --git a/src/Services/ExecutorService.php b/src/Services/ExecutorService.php
deleted file mode 100644
index 8b23727..0000000
--- a/src/Services/ExecutorService.php
+++ /dev/null
@@ -1,122 +0,0 @@
-dir = base_path('executors');
- }
-
- /**
- * Check if the executors directory exists.
- *
- * @return bool
- */
- public function isExists()
- {
- return is_dir($this->dir);
- }
-
- /**
- * Get the executors.
- *
- * @return \Illuminate\Support\Collection
- */
- protected function prepareExecutors()
- {
- $db = Executor::all()->keyBy('executor');
-
- return collect(array_map(function ($file) use ($db) {
- $class = new ReflectionClass(include $file);
- $name = basename($class->getFileName(), '.php');
- $executor = [
- 'name' => $name,
- 'type' => $class->getProperty('type')->getDefaultValue(),
- 'tag' => $class->getProperty('tag')->getDefaultValue(),
- 'path' => $class->getFileName(),
- 'model' => $db[$name] ?? null
- ];
-
- if ($executor['model']) {
- $executor['model']->fill([
- 'type' => $executor['type'],
- 'tag' => $executor['tag'],
- ]);
-
- if ($executor['model']->isDirty()) {
- $executor['model']->save();
- }
- }
- return $executor;
- }, $this->getPaths()))->keyBy('name');
- }
-
-
- /**
- * Sync the executors.
- *
- * @return \Illuminate\Support\Collection
- */
- public function sync()
- {
- $executors = $this->prepareExecutors();
-
- $newExecutors = $executors->filter(fn ($executor) => $executor['model'] == null);
- if ($newExecutors->isNotEmpty()) {
- $batch = $this->getNextBatch();
-
- $newExecutors->each(function ($executor) use (&$executors, $batch) {
- $executor['model'] = new Executor([
- 'executor' => $executor['name'],
- 'type' => $executor['type'],
- 'tag' => $executor['tag'],
- 'batch' => $batch,
- ]);
-
- $executors->offsetSet($executor['name'], $executor);
- });
- }
-
- return $executors;
- }
-
- /**
- * Get the next batch number.
- *
- * @return int
- */
- protected function getNextBatch()
- {
- return (Executor::orderBy('batch', 'desc')->first()?->batch ?? 0) + 1;
- }
-
- /**
- * Get the paths of the executors.
- *
- * @return array
- */
- protected function getPaths()
- {
- $collectPath = collect([]);
-
- foreach (ExecutorPool::getPaths() as $path) {
- $collectPath = $collectPath->merge(File::glob($path . '/*'));
- }
-
- return $collectPath->all();
- }
-}
diff --git a/src/Traits/InteractsWithIO.php b/src/Traits/InteractsWithIO.php
deleted file mode 100644
index 2755c14..0000000
--- a/src/Traits/InteractsWithIO.php
+++ /dev/null
@@ -1,331 +0,0 @@
- OutputInterface::VERBOSITY_VERBOSE,
- 'vv' => OutputInterface::VERBOSITY_VERY_VERBOSE,
- 'vvv' => OutputInterface::VERBOSITY_DEBUG,
- 'quiet' => OutputInterface::VERBOSITY_QUIET,
- 'normal' => OutputInterface::VERBOSITY_NORMAL,
- ];
-
- /**
- * Confirm a question with the user.
- *
- * @param string $question
- * @param bool $default
- * @return bool
- */
- public function confirm($question, $default = false)
- {
- return $this->output->confirm($question, $default);
- }
-
- /**
- * Prompt the user for input.
- *
- * @param string $question
- * @param string|null $default
- * @return mixed
- */
- public function ask($question, $default = null)
- {
- return $this->output->ask($question, $default);
- }
-
- /**
- * Prompt the user for input with auto completion.
- *
- * @param string $question
- * @param array|callable $choices
- * @param string|null $default
- * @return mixed
- */
- public function anticipate($question, $choices, $default = null)
- {
- return $this->askWithCompletion($question, $choices, $default);
- }
-
- /**
- * Prompt the user for input with auto completion.
- *
- * @param string $question
- * @param array|callable $choices
- * @param string|null $default
- * @return mixed
- */
- public function askWithCompletion($question, $choices, $default = null)
- {
- $question = new Question($question, $default);
-
- is_callable($choices)
- ? $question->setAutocompleterCallback($choices)
- : $question->setAutocompleterValues($choices);
-
- return $this->output->askQuestion($question);
- }
-
- /**
- * Prompt the user for input but hide the answer from the console.
- *
- * @param string $question
- * @param bool $fallback
- * @return mixed
- */
- public function secret($question, $fallback = true)
- {
- $question = new Question($question);
-
- $question->setHidden(true)->setHiddenFallback($fallback);
-
- return $this->output->askQuestion($question);
- }
-
- /**
- * Give the user a single choice from an array of answers.
- *
- * @param string $question
- * @param array $choices
- * @param string|int|null $default
- * @param mixed|null $attempts
- * @param bool $multiple
- * @return string|array
- */
- public function choice($question, array $choices, $default = null, $attempts = null, $multiple = false)
- {
- $question = new ChoiceQuestion($question, $choices, $default);
-
- $question->setMaxAttempts($attempts)->setMultiselect($multiple);
-
- return $this->output->askQuestion($question);
- }
-
- /**
- * Format input to textual table.
- *
- * @param array $headers
- * @param \Illuminate\Contracts\Support\Arrayable|array $rows
- * @param \Symfony\Component\Console\Helper\TableStyle|string $tableStyle
- * @param array $columnStyles
- * @return void
- */
- public function table($headers, $rows, $tableStyle = 'default', array $columnStyles = [])
- {
- $table = new Table($this->output);
-
- if ($rows instanceof Arrayable) {
- $rows = $rows->toArray();
- }
-
- $table->setHeaders((array) $headers)->setRows($rows)->setStyle($tableStyle);
-
- foreach ($columnStyles as $columnIndex => $columnStyle) {
- $table->setColumnStyle($columnIndex, $columnStyle);
- }
-
- $table->render();
- }
-
- /**
- * Execute a given callback while advancing a progress bar.
- *
- * @param iterable|int $totalSteps
- * @param \Closure $callback
- * @return mixed|void
- */
- public function withProgressBar($totalSteps, Closure $callback)
- {
- $bar = $this->output->createProgressBar(
- is_iterable($totalSteps) ? count($totalSteps) : $totalSteps
- );
-
- $bar->start();
-
- if (is_iterable($totalSteps)) {
- foreach ($totalSteps as $value) {
- $callback($value, $bar);
-
- $bar->advance();
- }
- } else {
- $callback($bar);
- }
-
- $bar->finish();
-
- if (is_iterable($totalSteps)) {
- return $totalSteps;
- }
- }
-
- /**
- * Write a string as information output.
- *
- * @param string $string
- * @param int|string|null $verbosity
- * @return void
- */
- public function info($string, $verbosity = null)
- {
- $this->line($string, 'info', $verbosity);
- }
-
- /**
- * Write a string as standard output.
- *
- * @param string $string
- * @param string|null $style
- * @param int|string|null $verbosity
- * @return void
- */
- public function line($string, $style = null, $verbosity = null)
- {
- $styled = $style ? "<$style>$string$style>" : $string;
-
- $this->output->writeln($styled, $this->parseVerbosity($verbosity));
- }
-
- /**
- * Write a string as comment output.
- *
- * @param string $string
- * @param int|string|null $verbosity
- * @return void
- */
- public function comment($string, $verbosity = null)
- {
- $this->line($string, 'comment', $verbosity);
- }
-
- /**
- * Write a string as question output.
- *
- * @param string $string
- * @param int|string|null $verbosity
- * @return void
- */
- public function question($string, $verbosity = null)
- {
- $this->line($string, 'question', $verbosity);
- }
-
- /**
- * Write a string as error output.
- *
- * @param string $string
- * @param int|string|null $verbosity
- * @return void
- */
- public function error($string, $verbosity = null)
- {
- $this->line($string, 'error', $verbosity);
- }
-
- /**
- * Write a string as warning output.
- *
- * @param string $string
- * @param int|string|null $verbosity
- * @return void
- */
- public function warn($string, $verbosity = null)
- {
- if (! $this->output->getFormatter()->hasStyle('warning')) {
- $style = new OutputFormatterStyle('yellow');
-
- $this->output->getFormatter()->setStyle('warning', $style);
- }
-
- $this->line($string, 'warning', $verbosity);
- }
-
- /**
- * Write a string in an alert box.
- *
- * @param string $string
- * @param int|string|null $verbosity
- * @return void
- */
- public function alert($string, $verbosity = null)
- {
- $length = Str::length(strip_tags($string)) + 12;
-
- $this->comment(str_repeat('*', $length), $verbosity);
- $this->comment('* '.$string.' *', $verbosity);
- $this->comment(str_repeat('*', $length), $verbosity);
-
- $this->comment('', $verbosity);
- }
-
- /**
- * Write a blank line.
- *
- * @param int $count
- * @return $this
- */
- public function newLine($count = 1)
- {
- $this->output->newLine($count);
-
- return $this;
- }
-
- /**
- * Set the verbosity level.
- *
- * @param string|int $level
- * @return void
- */
- protected function setVerbosity($level)
- {
- $this->verbosity = $this->parseVerbosity($level);
- }
-
- /**
- * Get the verbosity level in terms of Symfony's OutputInterface level.
- *
- * @param string|int|null $level
- * @return int
- */
- protected function parseVerbosity($level = null)
- {
- if (isset($this->verbosityMap[$level])) {
- $level = $this->verbosityMap[$level];
- } elseif (! is_int($level)) {
- $level = $this->verbosity;
- }
-
- return $level;
- }
-}
diff --git a/stubs/executor.php.stub b/stubs/executor.php.stub
index 9c51a4a..4b09d23 100644
--- a/stubs/executor.php.stub
+++ b/stubs/executor.php.stub
@@ -1,19 +1,47 @@
assertExitCode(0);
}
- public function testMakeOnceExecutor()
- {
- $this->artisan('execute:make', [
- 'name' => 'testMakeOnceExecutor',
- '--once' => true,
- ])
- ->assertExitCode(0);
- }
-
- public function testMakeExecutorWithTag()
- {
- $this->artisan('execute:make', [
- 'name' => 'testMakeExecutorWithTag',
- '--tag' => 'test',
- ])
- ->assertExitCode(0);
- }
-
- public function testMakeOnceExecutorWithTag()
- {
- $this->artisan('execute:make', [
- 'name' => 'testMakeOnceExecutorWithTag',
- '--once' => true,
- '--tag' => 'test',
- ])
- ->assertExitCode(0);
- }
-
public function testExecute()
{
- $this->testMakeExecutor();
+ $this->artisan('execute:make', ['name' => 'testMakeExecutor']);
$this->artisan('execute')->assertOk();
$this->assertEquals(1, Executor::count());
}
public function testRollbackSuccess()
{
- $this->testMakeExecutor();
+ $this->artisan('execute:make', ['name' => 'testMakeExecutor']);
$this->artisan('execute')->assertOk();
$this->artisan('execute:rollback')->assertOk();
$this->assertEquals(0, Executor::count());
@@ -61,35 +33,35 @@ public function testRollbackSuccess()
public function testRollbackFailed()
{
- $this->artisan('execute:rollback')->assertFailed();
+ $this->artisan('execute:rollback')
+ ->expectsOutput('There are no executors has been found.')
+ ->assertExitCode(0);
}
public function testFreshExecutors()
{
- $this->testMakeExecutor();
+ $this->artisan('execute:make', ['name' => 'testMakeExecutor']);
$this->artisan('execute:fresh')->assertOk();
$this->assertEquals(1, Executor::count());
}
public function testStatusOfExecutors()
{
- $this->testMakeExecutor();
-
- $executors = (new ExecutorService)->sync();
-
+ $this->artisan('execute:make', ['name' => 'testMakeExecutor']);
$this->artisan('execute:status')
->assertOk()
->expectsTable(
- ['Name', 'Type', 'Tag', 'Batch', 'Executed'],
- $executors->map(function ($executor) {
- return [
- $executor['name'],
- ucfirst($executor['type']->name),
- $executor['tag'],
- $executor['model']->batch,
- $executor['model']->executed > 0 ? 'Yes' : 'No',
- ];
- })
+ ['Name', 'Type', 'Tags', 'Servers', 'Batch', 'Executed'],
+ [
+ [
+ basename(File::files(base_path('executors'))[0]->getBasename(), '.php'),
+ 'Always',
+ 'None',
+ 'All',
+ 'N/A',
+ 'No',
+ ],
+ ]
);
}
}
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 8998d59..820e273 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -34,11 +34,11 @@ protected function getPackageProviders($app)
*/
protected function getEnvironmentSetUp($app)
{
- $app['config']->set('database.default', 'testbench');
- $app['config']->set('database.connections.testbench', [
- 'driver' => 'sqlite',
+ $app['config']->set('database.default', 'testing');
+ $app['config']->set('database.connections.testing', [
+ 'driver' => 'sqlite',
'database' => ':memory:',
- 'prefix' => '',
+ 'prefix' => '',
]);
}
}