Керівництво розробника
Створення власних воркерів
Для створення власного воркера, розширте клас BaseWorker який надає всю спільну функціональність:
<?php namespace YourNamespace\Workers;
use Seiger\sTask\Workers\BaseWorker;
use Seiger\sTask\Models\sTaskModel;
class ProductWorker extends BaseWorker
{
/**
* Унікальний ідентифікатор для цього воркера
*/
public function identifier(): string
{
return 'product';
}
/**
* Scope модуля/пакету (для фільтрації в адміні)
*/
public function scope(): string
{
return 'scommerce';
}
/**
* Іконка для адмін інтерфейсу
*/
public function icon(): string
{
return '<i class="fa fa-cube"></i>';
}
/**
* Коротка зрозуміла назва
*/
public function title(): string
{
return 'Управління товарами';
}
/**
* Детальний опис
*/
public function description(): string
{
return 'Імпорт та експорт товарів з/до CSV файлів';
}
/**
* Рендер віджета для адмін інтерфейсу
*/
public function renderWidget(): string
{
return view('your-package::widgets.product-worker', [
'worker' => $this
])->render();
}
/**
* Налаштування воркера (опціонально)
*/
public function settings(): array
{
return [
'batch_size' => 100,
'timeout' => 3600,
'retry_on_fail' => true,
];
}
/**
* Дія: Імпорт товарів з CSV
*/
public function taskImport(sTaskModel $task, array $options = []): void
{
try {
// Оновити статус задачі
$task->update(['status' => 20, 'message' => 'Початок імпорту...']);
// Отримати файл з опцій
$file = $options['file'] ?? null;
if (!$file || !file_exists($file)) {
throw new \Exception('Файл для імпорту не знайдено');
}
// Прочитати CSV
$handle = fopen($file, 'r');
$header = fgetcsv($handle);
// Порахувати загальну кількість рядків
$total = 0;
while (fgets($handle)) $total++;
rewind($handle);
fgetcsv($handle); // П ропустити заголовок
$processed = 0;
$startTime = microtime(true);
// Обробити кожен рядок
while (($row = fgetcsv($handle)) !== false) {
$data = array_combine($header, $row);
// Логіка імпорту товару
$this->importProduct($data);
$processed++;
// Оновлювати прогрес кожні 10 елементів
if ($processed % 10 === 0 || $processed === $total) {
$progress = (int)(($processed / $total) * 100);
// Розрахувати ETA
$elapsed = microtime(true) - $startTime;
$rate = $processed / $elapsed;
$remaining = $total - $processed;
$etaSeconds = $remaining > 0 ? $remaining / $rate : 0;
$this->pushProgress($task, [
'progress' => $progress,
'processed' => $processed,
'total' => $total,
'eta' => niceEta($etaSeconds),
'message' => "Імпортовано {$processed} з {$total} товарів"
]);
}
}
fclose($handle);
// Позначити як завершене
$this->markFinished(
$task,
null,
"Успішно імпортовано {$processed} товарів за " . round(microtime(true) - $startTime, 2) . "с"
);
} catch (\Exception $e) {
$this->markFailed($task, $e->getMessage());
}
}
/**
* Дія: Експорт товарів до CSV
*/
public function taskExport(sTaskModel $task, array $options = []): void
{
try {
$task->update(['status' => 20, 'message' => 'Початок експорту...']);
// Підготувати файл експорту
$filename = 'products_' . date('Y-m-d_His') . '.csv';
$filepath = storage_path('stask/uploads/' . $filename);
if (!is_dir(dirname($filepath))) {
mkdir(dirname($filepath), 0755, true);
}
$handle = fopen($filepath, 'w');
// Записати заголовок
fputcsv($handle, ['ID', 'SKU', 'Назва', 'Ціна', 'Залишок']);
// Отримати товари
$products = \DB::table('products')->get();
$total = count($products);
$processed = 0;
foreach ($products as $i => $product) {
fputcsv($handle, [
$product->id,
$product->sku,
$product->name,
$product->price,
$product->stock,
]);
$processed++;
if ($processed % 100 === 0 || $processed === $total) {
$progress = (int)(($processed / $total) * 100);
$this->pushProgress($task, [
'progress' => $progress,
'processed' => $processed,
'total' => $total,
'message' => "Експортовано {$processed} з {$total} товарів"
]);
}
}
fclose($handle);
$this->markFinished(
$task,
$filepath,
"Експортовано {$total} товарів до {$filename}"
);
} catch (\Exception $e) {
$this->markFailed($task, $e->getMessage());
}
}
/**
* Дія: Синхронізація залишків
*/
public function taskSyncStock(sTaskModel $task, array $options = []): void
{
try {
$source = $options['source'] ?? 'api';
$task->update(['status' => 20, 'message' => "Синхронізація залишків з {$source}..."]);
// Ваша логіка синхронізації тут
$items = $this->fetchStockFromSource($source);
$total = count($items);
foreach ($items as $i => $item) {
$this->updateProductStock($item['sku'], $item['quantity']);
if (($i + 1) % 50 === 0) {
$this->pushProgress($task, [
'progress' => (int)((($i + 1) / $total) * 100),
'processed' => $i + 1,
'total' => $total,
]);
}
}
$this->markFinished($task, null, "Синхронізовано залишки для {$total} товарів");
} catch (\Exception $e) {
$this->markFailed($task, $e->getMessage());
}
}
// Допоміжні методи
private function importProduct(array $data): void
{
// Ваша логіка імпорту
}
private function fetchStockFromSource(string $source): array
{
// Ваша логіка отримання з API/джерела
return [];
}
private function updateProductStock(string $sku, int $quantity): void
{
// Логіка оновлення
}
}
Автоматичне виявлення воркерів
Воркери автоматично виявляються якщо вони:
- Реалізують інтерфейс
TaskInterface - Не є абстрактними класами
- Можуть бути інстанційовані
- Знаходяться в просторі імен вашого пакету
Процес виявлення сканує всі встановлені Composer пакети та автоматично реєструє воркерів.
Конфігурація воркерів
Кастомні налаштування
Воркери можуть надавати власну конфігурацію через метод renderSettings():
public function renderSettings(): string
{
$apiKey = $this->getConfig('api_key', '');
$endpoint = $this->getConfig('endpoint', '');
return <<<HTML
<h4><i data-lucide="key" class="w-4 h-4"></i> Конфігурація API</h4>
<div class="form-group">
<label>API Endpoint</label>
<input type="url"
class="form-control"
name="endpoint"
value="{$endpoint}"
placeholder="https://api.example.com">
</div>
<div class="form-group">
<label>API ключ</label>
<input type="text"
class="form-control"
name="api_key"
value="{$apiKey}"
placeholder="ваш-api-ключ">
</div>
<hr>
HTML;
}
Читання конфігурації
Використовуйте методи BaseWorker для доступу до налаштувань:
// Отримати одне значення
$endpoint = $this->getConfig('endpoint', 'https://default.com');
// Отримати вкладене значення (dot notation)
$timeout = $this->getConfig('api.timeout', 30);
// Отримати всі налаштування
$settings = $this->settings();
Збереження конфігурації
Конфігурація автоматично зберігається через адмін інтерфейс. Також можна програмно оновити:
// Встановити одне значення
$this->setConfig('endpoint', 'https://api.example.com');
// Оновити кілька значень
$this->updateConfig([
'endpoint' => 'https://api.example.com',
'api_key' => 'secret-key',
'timeout' => 60,
]);
Зберігання: Налаштування зберігаються в s_workers.settings (JSON колонка).
Конфігурація розкладу
Воркери з методом taskMake() автоматично отримують конфігурацію розкладу в адмін інтерфейсі.
Типи розкладу:
- На вимогу - Тільки ручне виконання
- Один раз - Виконати один раз у вказаний datetime
- Періодично - Виконувати у вказаний час з періодичністю (щогодини/щодня/щотижня)
- Регулярно - Виконувати у часовому періоді з інтервалом (кожні 15/30/60 хвилин)
Перевірка чи треба запускати:
public function taskMake(sTaskModel $task, array $opt = []): void
{
// Перевірка розкладу (пропускаємо для ручних запусків)
$isManual = $opt['manual'] ?? true;
if (!$isManual && !$this->shouldRunNow()) {
$task->update([
'status' => sTaskModel::TASK_STATUS_FINISHED,
'message' => 'Пропущено: поза межами розкладу',
]);
return;
}
// Продовження виконання задачі...
}
Доступ до розкладу:
$schedule = $this->getSchedule();
// Повертає:
// [
// 'type' => 'regular',
// 'enabled' => true,
// 'start_time' => '05:00',
// 'end_time' => '23:00',
// 'interval' => 'hourly',
// ]
API управління задачами
Створення задач
use Seiger\sTask\Facades\sTask;
// Базове створення задачі
$task = sTask::create(
identifier: 'product',
action: 'import',
data: ['file' => '/path/to/products.csv'],
priority: 'high',
userId: evo()->getLoginUserID()
);
// Створити з власним пріоритетом
$task = sTask::create(
identifier: 'product',
action: 'export',
data: ['format' => 'csv', 'filters' => ['active' => true]],
priority: 'normal', // 'low', 'normal', 'high'
userId: evo()->getLoginUserID()
);
// Програмне створення задачі з воркера
$worker = new ProductWorker();
$task = $worker->createTask('import', ['file' => 'products.csv']);
Обробка задач
// Обробити всі задачі що очікують (розмір пакету за замовчуванням: 10)
$processedCount = sTask::processPendingTasks();
// Обробити з власним розміром пакету
$processedCount = sTask::processPendingTasks(batchSize: 50);
// Отримати статистику задач
$stats = sTask::getStats();
/* Повертає:
[
'pending' => 5,
'running' => 2,
'completed' => 100,
'failed' => 3,
'cancelled' => 1,
'total' => 111,
]
*/
// Отримати задачі що очікують
$pending = sTask::getPendingTasks(limit: 20);
foreach ($pending as $task) {
echo "Задача #{$task->id}: {$task->identifier} -> {$task->action}\n";
}
Управління воркерами
// Виявити нові воркери
$registered = sTask::discoverWorkers();
echo "Зареєстровано " . count($registered) . " нових воркерів\n";
// Пересканувати існуючі воркери (оновити їх метадані)
$updated = sTask::rescanWorkers();
echo "Оновлено " . count($updated) . " воркерів\n";
// Очистити orphaned воркери (класи більше не існують)
$deleted = sTask::cleanOrphanedWorkers();
echo "Видалено {$deleted} orphaned воркерів\n";
// Отримати всіх воркерів
$workers = sTask::getWorkers(activeOnly: false);
foreach ($workers as $worker) {
echo "{$worker->identifier} ({$worker->scope}) - ";
echo $worker->active ? 'Активний' : 'Неактивний';
echo "\n";
}
// Отримати конкретного воркера
$worker = sTask::getWorker('product');
if ($worker) {
echo "Назва: {$worker->title}\n";
echo "Опис: {$worker->description}\n";
echo "Іконка: {$worker->icon}\n";
}
// Активувати/деактивувати воркерів
sTask::activateWorker('product');
sTask::deactivateWorker('old_worker');
// Фільтрувати воркерів за scope
$commerceWorkers = \Seiger\sTask\Models\sWorker::byScope('scommerce')->get();
Виконання задач
// Виконати конкретну задачу
$task = \Seiger\sTask\Models\sTaskModel::find(1);
$result = sTask::execute($task);
if ($result) {
echo "Задача завершена успішно\n";
} else {
echo "Задача невдала: {$task->message}\n";
}
// Повторити невдалу задачу
if ($task->canRetry()) {
sTask::retry($task);
}
Операції очищення
// Очистити задачі старші 30 днів
$deletedTasks = sTask::cleanOldTasks(days: 30);
echo "Видалено {$deletedTasks} старих задач\n";
// Очистити логи старші 30 днів
$deletedLogs = sTask::cleanOldLogs(days: 30);
echo "Видалено {$deletedLogs} старих файлів логів\n";
// Власне очищення
$deleted = \Seiger\sTask\Models\sTaskModel::where('status', 30) // completed
->where('finished_at', '<', now()->subDays(7))
->delete();