承接 azaharizaman/nexus-scheduler 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

azaharizaman/nexus-scheduler

Composer 安装命令:

composer require azaharizaman/nexus-scheduler

包简介

Framework-agnostic job scheduling engine for managing future-dated instructions with handler-based execution

README 文档

README

A framework-agnostic job scheduling engine that manages future-dated instructions through contracts, delegating execution to domain packages and persistence to the application layer.

Overview

The Nexus\Scheduler package serves as the central repository for future-dated instructions. It manages when an action should happen, but delegates what the action is (domain logic) and how the action is executed (job runner/queue).

Core Principles

  1. Complete Decoupling: Stateless design with all persistence via ScheduleRepositoryInterface
  2. Execution Agnostic: Unaware of runtime environment (Laravel Queue, pure Cron, etc.)
  3. Handler-Based: Domain packages implement JobHandlerInterface for their specific job types
  4. Time Control: Testable via ClockInterface injection
  5. Hybrid Retry: Handlers signal intent, engine manages execution

Architecture

The Scheduling Paradigm

┌─────────────────────────────────────────────────────────────────┐
│                      DOMAIN PACKAGE                              │
│  (e.g., Nexus\Export, Nexus\Workflow)                           │
│                                                                   │
│  Calls: $scheduler->schedule(ScheduleDefinition)                │
│  Implements: ExportReportHandler implements JobHandlerInterface │
│  Tags: $app->tag([ExportReportHandler::class], 'scheduler.han..│
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                   NEXUS\SCHEDULER (THIS PACKAGE)                │
│                                                                   │
│  ScheduleManager ──► ExecutionEngine ──► JobHandlerInterface   │
│         │                    │                                   │
│         │                    └──► Interprets JobResult          │
│         │                         (shouldRetry, retryDelay)      │
│         │                                                        │
│         └──► RecurrenceEngine (cron, intervals)                 │
│                                                                   │
│  Contracts: ScheduleRepositoryInterface, JobQueueInterface      │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                     APPLICATION LAYER                            │
│                      (Nexus\Atomy)                               │
│                                                                   │
│  DbScheduleRepository ──► Eloquent Model                        │
│  LaravelJobQueue ──► Laravel Queue System                       │
│  SystemClock ──► DateTimeImmutable                              │
│  Tagged Handler Discovery ──► Service Provider                  │
└─────────────────────────────────────────────────────────────────┘

Execution Flow

  1. Scheduling Phase (Domain → Scheduler)

    • Domain package calls $scheduler->schedule(ScheduleDefinition)
    • ScheduleManager creates ScheduledJob value object
    • Persists via ScheduleRepositoryInterface
  2. Execution Phase (Cron → Queue → Handler)

    • External cron job calls ProcessScheduledJobs command
    • Command retrieves due jobs via $manager->getDueJobs()
    • For each job, dispatches to queue via JobQueueInterface
    • Queue worker invokes appropriate JobHandlerInterface::handle()
    • Handler returns JobResult with retry intent
    • ExecutionEngine updates status and re-queues if needed
  3. Retry Strategy (Hybrid Delegation)

    • Handler decides: "Should retry? Custom delay?"
    • Engine executes: Status update, queue dispatch, exponential backoff
    • Clean separation: Domain logic (intent) vs. Infrastructure (mechanism)

Installation

1. Install Package

composer require azaharizaman/nexus-scheduler:*@dev

2. Optional: Install Cron Expression Support

composer require dragonmantank/cron-expression

3. Implement Required Contracts in Your Application

// app/Repositories/DbScheduleRepository.php
class DbScheduleRepository implements ScheduleRepositoryInterface
{
    public function save(ScheduledJobInterface $job): void { /* ... */ }
    public function findDue(DateTimeImmutable $asOf): array { /* ... */ }
    // ... other methods
}

// app/Services/LaravelJobQueue.php
class LaravelJobQueue implements JobQueueInterface
{
    public function dispatch(ScheduledJobInterface $job, ?int $delaySeconds = null): void
    {
        // Dispatch to Laravel queue
    }
}

// app/Services/SystemClock.php
class SystemClock implements ClockInterface
{
    public function now(): DateTimeImmutable
    {
        return new DateTimeImmutable();
    }
}

4. Bind Interfaces in Service Provider

// app/Providers/SchedulerServiceProvider.php
public function register(): void
{
    // Bind infrastructure
    $this->app->singleton(ScheduleRepositoryInterface::class, DbScheduleRepository::class);
    $this->app->singleton(JobQueueInterface::class, LaravelJobQueue::class);
    $this->app->singleton(ClockInterface::class, SystemClock::class);
    $this->app->singleton(CalendarExporterInterface::class, NullCalendarExporter::class);
    
    // Inject tagged handlers into ScheduleManager
    $this->app->singleton(ScheduleManager::class, function ($app) {
        return new ScheduleManager(
            repository: $app->make(ScheduleRepositoryInterface::class),
            queue: $app->make(JobQueueInterface::class),
            clock: $app->make(ClockInterface::class),
            handlers: $app->tagged('scheduler.handlers'),
            logger: $app->make(LoggerInterface::class)
        );
    });
}

Usage

1. Schedule a Job

use Nexus\Scheduler\Services\ScheduleManager;
use Nexus\Scheduler\ValueObjects\ScheduleDefinition;
use Nexus\Scheduler\Enums\JobType;
use Nexus\Scheduler\ValueObjects\ScheduleRecurrence;

$scheduler = app(ScheduleManager::class);

// One-time job
$job = $scheduler->schedule(new ScheduleDefinition(
    jobType: JobType::EXPORT_REPORT,
    targetId: '01JCV9X...',  // ULID of entity
    runAt: new DateTimeImmutable('+1 hour'),
    payload: ['format' => 'pdf', 'templateId' => '01JCV...']
));

// Recurring job (daily at 9 AM)
$job = $scheduler->schedule(new ScheduleDefinition(
    jobType: JobType::DOCUMENT_SHREDDING,
    targetId: '01JCV9Y...',
    runAt: new DateTimeImmutable('tomorrow 09:00'),
    recurrence: new ScheduleRecurrence(
        type: RecurrenceType::DAILY,
        interval: 1
    ),
    payload: ['retentionDays' => 90]
));

// Cron-based recurrence (every Monday at 8 AM)
$job = $scheduler->schedule(new ScheduleDefinition(
    jobType: JobType::WORK_ORDER_START,
    targetId: '01JCV9Z...',
    runAt: new DateTimeImmutable('next Monday 08:00'),
    recurrence: new ScheduleRecurrence(
        type: RecurrenceType::CRON,
        cronExpression: '0 8 * * 1'
    )
));

2. Implement a Job Handler

namespace App\Handlers;

use Nexus\Scheduler\Contracts\JobHandlerInterface;
use Nexus\Scheduler\ValueObjects\ScheduledJob;
use Nexus\Scheduler\ValueObjects\JobResult;
use Nexus\Scheduler\Enums\JobType;

class ExportReportHandler implements JobHandlerInterface
{
    public function __construct(
        private readonly ExportService $exporter
    ) {}
    
    public function supports(JobType $jobType): bool
    {
        return $jobType === JobType::EXPORT_REPORT;
    }
    
    public function handle(ScheduledJob $job): JobResult
    {
        try {
            $this->exporter->generate(
                templateId: $job->payload['templateId'],
                format: $job->payload['format']
            );
            
            return JobResult::success(
                output: ['fileUrl' => 'https://...']
            );
            
        } catch (TemporaryFailureException $e) {
            // Retry with custom 5-minute delay
            return JobResult::failure(
                error: $e->getMessage(),
                shouldRetry: true,
                retryDelaySeconds: 300
            );
            
        } catch (PermanentFailureException $e) {
            // Don't retry
            return JobResult::failure(
                error: $e->getMessage(),
                shouldRetry: false
            );
        }
    }
}

3. Register Handler (Tagged Service)

// app/Providers/ExportServiceProvider.php
public function register(): void
{
    // Tag handler for automatic discovery
    $this->app->tag([ExportReportHandler::class], 'scheduler.handlers');
}

4. Process Scheduled Jobs (Cron)

// app/Console/Commands/ProcessScheduledJobs.php
public function handle(ScheduleManager $scheduler): int
{
    $dueJobs = $scheduler->getDueJobs();
    
    foreach ($dueJobs as $job) {
        // Dispatches to queue, queue worker invokes handler
        $scheduler->executeJob($job->id);
    }
    
    return 0;
}

Add to your cron:

* * * * * php artisan schedule:process

Handler Registration Pattern

The package uses tagged services for handler discovery, avoiding tight coupling to concrete domain classes.

Domain Package Responsibility

  1. Implement JobHandlerInterface
  2. Tag in service provider: $app->tag([YourHandler::class], 'scheduler.handlers')

Scheduler Package Responsibility

  1. Receive iterable $handlers via constructor injection
  2. Build internal JobType => HandlerInterface mapping
  3. Dispatch to appropriate handler based on job type

Zero coupling: The scheduler never knows concrete handler class names.

Retry Strategy

Hybrid Delegation Model

Handler (Domain Logic): Decides retry intent

return JobResult::failure(
    error: 'API rate limit exceeded',
    shouldRetry: true,
    retryDelaySeconds: 300  // Custom 5-minute delay
);

ExecutionEngine (Infrastructure): Manages retry mechanism

  • Updates job status (FAILEDPENDING)
  • Increments retry_count
  • Dispatches to queue with specified delay
  • Applies exponential backoff if no custom delay
  • Marks as FAILED_PERMANENT if shouldRetry === false

Decision Flowchart

JobHandler::handle() returns JobResult
              │
              ▼
    ┌─────────────────────┐
    │  shouldRetry: bool  │
    └─────────────────────┘
              │
         ┌────┴────┐
         │         │
        YES        NO
         │         │
         │         └──► Mark FAILED_PERMANENT
         │
         ▼
┌────────────────────────┐
│ retryDelaySeconds: ?int│
└────────────────────────┘
         │
    ┌────┴────┐
    │         │
  NULL     CUSTOM
    │         │
    │         └──► Re-queue with custom delay
    │
    └──► Apply exponential backoff
         (60s, 120s, 240s, ...)

Value Objects

ScheduledJob

Immutable representation of a scheduled job with business logic methods:

$job->isDue($clock);           // bool: Is it time to run?
$job->isOverdue($clock);       // bool: Past runAt time?
$job->getNextRunTime($clock);  // ?DateTimeImmutable: Next recurrence
$job->canExecute();            // bool: Is status PENDING?

ScheduleRecurrence

Defines repetition rules:

// Simple interval
new ScheduleRecurrence(
    type: RecurrenceType::DAILY,
    interval: 2  // Every 2 days
);

// Cron expression (requires dragonmantank/cron-expression)
new ScheduleRecurrence(
    type: RecurrenceType::CRON,
    cronExpression: '0 9 * * 1-5'  // Weekdays at 9 AM
);

JobResult

Handler's response with retry intent:

// Success
JobResult::success(output: ['recordsProcessed' => 150]);

// Retriable failure
JobResult::failure(
    error: 'Connection timeout',
    shouldRetry: true,
    retryDelaySeconds: 60
);

// Permanent failure
JobResult::failure(
    error: 'Invalid configuration',
    shouldRetry: false
);

Enums

JobType

Extensible enum for domain-specific job types:

enum JobType: string
{
    case EXPORT_REPORT = 'export_report';
    case DOCUMENT_SHREDDING = 'document_shredding';
    case WORK_ORDER_START = 'work_order_start';
    case SEND_REMINDER = 'send_reminder';
    // Domain packages add their own types
}

JobStatus

Lifecycle state with transition validation:

enum JobStatus: string
{
    case PENDING = 'pending';
    case RUNNING = 'running';
    case COMPLETED = 'completed';
    case FAILED = 'failed';
    case FAILED_PERMANENT = 'failed_permanent';
    case CANCELED = 'canceled';
    
    public function canTransitionTo(JobStatus $newStatus): bool;
    public function canExecute(): bool;
    public function isFinal(): bool;
}

Testing

Time Control

Use ClockInterface for deterministic testing:

$mockClock = new class implements ClockInterface {
    private DateTimeImmutable $now;
    
    public function now(): DateTimeImmutable {
        return $this->now;
    }
    
    public function setTime(DateTimeImmutable $time): void {
        $this->now = $time;
    }
};

// Test isDue() logic
$mockClock->setTime(new DateTimeImmutable('2025-01-15 08:55:00'));
$this->assertFalse($job->isDue($mockClock));

$mockClock->setTime(new DateTimeImmutable('2025-01-15 09:00:00'));
$this->assertTrue($job->isDue($mockClock));

Running the Package Test Suite

The Scheduler package now ships with a dedicated PHPUnit 11 smoke suite plus deterministic in-memory adapters under tests/Support/. Run it locally before contributing new functionality:

cd packages/Scheduler
composer install        # once, to pull phpunit/phpunit via require-dev
composer test           # executes phpunit -c phpunit.xml.dist
# composer test:coverage  # optional: generates code coverage when Xdebug/PCOV is enabled

What the suite covers today:

  • ScheduleManagerTest validates successful dispatch, permanent-failure handling, and recurring job rollover.
  • Support doubles (MutableClock, InMemoryScheduleRepository, TrackingJobQueue, CallbackJobHandler) keep tests framework-agnostic.

Future work will extend coverage across value objects, the recurrence engine, and execution engine edge cases. Contributions should include corresponding tests that reuse the provided adapters or add new deterministic doubles inside tests/Support/.

Future Features (v2)

Calendar Export

The CalendarExporterInterface is defined but bound to NullCalendarExporter (throws FeatureNotImplementedException) in v1.

Planned v2 features:

  • Generate iCal files for scheduled jobs
  • Google Calendar URL generation
  • Outlook integration

📖 Documentation

Package Documentation

Additional Resources

  • IMPLEMENTATION_SUMMARY.md - Implementation progress
  • REQUIREMENTS.md - Requirements
  • TEST_SUITE_SUMMARY.md - Tests
  • VALUATION_MATRIX.md - Valuation

License

MIT License - see LICENSE file for details.

Support

For issues, questions, or contributions, please refer to the main Nexus ERP repository.

azaharizaman/nexus-scheduler 适用场景与选型建议

azaharizaman/nexus-scheduler 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 2 次下载、GitHub Stars 达 0, 最近一次更新时间为 2026 年 05 月 04 日, 在 PHP 生态内属于活跃度较高的组件。

我们在过去多个企业项目中使用过 azaharizaman/nexus-scheduler 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 azaharizaman/nexus-scheduler 我们能提供哪些服务?
定制开发 / 二次开发

基于 azaharizaman/nexus-scheduler 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 2
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 42
  • 依赖项目数: 3
  • 推荐数: 3

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-05-04