Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/Scheduler/TaskHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use CrazyGoat\WorkermanBundle\Event\TaskErrorEvent;
use CrazyGoat\WorkermanBundle\Event\TaskStartEvent;
use CrazyGoat\WorkermanBundle\Util\ServiceMethodHelper;
use Psr\Container\ContainerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

Expand All @@ -19,13 +20,18 @@ public function __construct(

public function __invoke(string $service, string $taskName): void
{
[$serviceName, $method] = explode('::', $service, 2);
[$serviceName, $method] = ServiceMethodHelper::split($service);
$service = $this->locator->get($serviceName);
assert(is_object($service));

$this->eventDispatcher->dispatch(new TaskStartEvent($service::class, $taskName));

try {
if (!method_exists($service, $method)) {
throw new \InvalidArgumentException(
sprintf('Method "%s" does not exist on service "%s" (class "%s").', $method, $serviceName, $service::class),
);
}
$service->$method();
} catch (\Throwable $e) {
$this->eventDispatcher->dispatch(new TaskErrorEvent($e, $service::class, $taskName));
Expand Down
8 changes: 7 additions & 1 deletion src/Supervisor/ProcessHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use CrazyGoat\WorkermanBundle\Event\ProcessErrorEvent;
use CrazyGoat\WorkermanBundle\Event\ProcessStartEvent;
use CrazyGoat\WorkermanBundle\Util\ServiceMethodHelper;
use Psr\Container\ContainerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

Expand All @@ -19,13 +20,18 @@ public function __construct(

public function __invoke(string $service, string $processName): void
{
[$serviceName, $method] = explode('::', $service, 2);
[$serviceName, $method] = ServiceMethodHelper::split($service);
$service = $this->locator->get($serviceName);
assert(is_object($service));

$this->eventDispatcher->dispatch(new ProcessStartEvent($service::class, $processName));

try {
if (!method_exists($service, $method)) {
throw new \InvalidArgumentException(
sprintf('Method "%s" does not exist on service "%s" (class "%s").', $method, $serviceName, $service::class),
);
}
$service->$method();
} catch (\Throwable $e) {
$this->eventDispatcher->dispatch(new ProcessErrorEvent($e, $service::class, $processName));
Expand Down
34 changes: 34 additions & 0 deletions src/Util/ServiceMethodHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace CrazyGoat\WorkermanBundle\Util;

/**
* @internal
*/
final class ServiceMethodHelper
{
private function __construct()
{
}

/**
* Validate and split a service method string into service ID and method name.
*
* Expected format: "serviceId::methodName"
*
* @return array{string, string} [serviceId, methodName]
*/
public static function split(string $service): array
{
$parts = explode('::', $service, 2);
if (\count($parts) !== 2 || $parts[0] === '' || $parts[1] === '') {
throw new \InvalidArgumentException(
sprintf('Invalid service method format "%s". Expected "serviceId::methodName".', $service),
);
}

return [$parts[0], $parts[1]];
}
}
123 changes: 123 additions & 0 deletions tests/Scheduler/TaskHandlerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

declare(strict_types=1);

namespace CrazyGoat\WorkermanBundle\Tests\Scheduler;

use CrazyGoat\WorkermanBundle\Event\TaskErrorEvent;
use CrazyGoat\WorkermanBundle\Event\TaskStartEvent;
use CrazyGoat\WorkermanBundle\Scheduler\TaskHandler;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

final class TaskHandlerTest extends TestCase
{
public function testInvokeDispatchesStartEventAndCallsServiceMethod(): void
{
$service = new class {
public bool $called = false;

public function execute(): void
{
$this->called = true;
}
};

$locator = $this->createMock(ContainerInterface::class);
$locator->method('get')->with('my_task_service')->willReturn($service);

$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
$eventDispatcher->expects($this->once())
->method('dispatch')
->with($this->isInstanceOf(TaskStartEvent::class));

$handler = new TaskHandler($locator, $eventDispatcher);
$handler->__invoke('my_task_service::execute', 'test_task');

$this->assertTrue($service->called);
}

public function testInvokeDispatchesErrorEventWhenMethodDoesNotExist(): void
{
$service = new class {
};

$locator = $this->createMock(ContainerInterface::class);
$locator->method('get')->with('my_task_service')->willReturn($service);

$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
$eventDispatcher->expects($this->exactly(2))
->method('dispatch')
->willReturnCallback(function ($event) {
static $callCount = 0;
++$callCount;
if ($callCount === 1) {
$this->assertInstanceOf(TaskStartEvent::class, $event);
} elseif ($callCount === 2) {
$this->assertInstanceOf(TaskErrorEvent::class, $event);
}

return $event;
});

$handler = new TaskHandler($locator, $eventDispatcher);
$handler->__invoke('my_task_service::nonexistent', 'test_task');
// Should not throw - error is caught by try-catch and dispatched as event
}

public function testInvokeDispatchesErrorEventWhenServiceMethodThrowsException(): void
{
$service = new class {
public function execute(): never
{
throw new \RuntimeException('Task failed');
}
};

$locator = $this->createMock(ContainerInterface::class);
$locator->method('get')->with('my_task_service')->willReturn($service);

$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
$eventDispatcher->expects($this->exactly(2))
->method('dispatch')
->willReturnCallback(function ($event) {
static $callCount = 0;
++$callCount;
if ($callCount === 1) {
$this->assertInstanceOf(TaskStartEvent::class, $event);
} elseif ($callCount === 2) {
$this->assertInstanceOf(TaskErrorEvent::class, $event);
$this->assertInstanceOf(\RuntimeException::class, $event->getError());
$this->assertSame('Task failed', $event->getError()->getMessage());
}

return $event;
});

$handler = new TaskHandler($locator, $eventDispatcher);
$handler->__invoke('my_task_service::execute', 'test_task');
}

/** @dataProvider provideInvalidServiceStrings */
public function testInvokeThrowsExceptionOnInvalidFormat(string $input): void
{
$locator = $this->createMock(ContainerInterface::class);
$eventDispatcher = $this->createMock(EventDispatcherInterface::class);

$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid service method format');

$handler = new TaskHandler($locator, $eventDispatcher);
$handler->__invoke($input, 'test_task');
}

/** @return iterable<array{string}> */
public static function provideInvalidServiceStrings(): iterable
{
yield 'missing separator' => ['JustAService'];
yield 'empty service ID' => ['::method'];
yield 'empty method name' => ['service::'];
yield 'empty string' => [''];
}
}
123 changes: 123 additions & 0 deletions tests/Supervisor/ProcessHandlerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

declare(strict_types=1);

namespace CrazyGoat\WorkermanBundle\Tests\Supervisor;

use CrazyGoat\WorkermanBundle\Event\ProcessErrorEvent;
use CrazyGoat\WorkermanBundle\Event\ProcessStartEvent;
use CrazyGoat\WorkermanBundle\Supervisor\ProcessHandler;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

final class ProcessHandlerTest extends TestCase
{
public function testInvokeDispatchesStartEventAndCallsServiceMethod(): void
{
$service = new class {
public bool $called = false;

public function run(): void
{
$this->called = true;
}
};

$locator = $this->createMock(ContainerInterface::class);
$locator->method('get')->with('my_service')->willReturn($service);

$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
$eventDispatcher->expects($this->once())
->method('dispatch')
->with($this->isInstanceOf(ProcessStartEvent::class));

$handler = new ProcessHandler($locator, $eventDispatcher);
$handler->__invoke('my_service::run', 'test_process');

$this->assertTrue($service->called);
}

public function testInvokeDispatchesErrorEventWhenMethodDoesNotExist(): void
{
$service = new class {
};

$locator = $this->createMock(ContainerInterface::class);
$locator->method('get')->with('my_service')->willReturn($service);

$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
$eventDispatcher->expects($this->exactly(2))
->method('dispatch')
->willReturnCallback(function ($event) {
static $callCount = 0;
++$callCount;
if ($callCount === 1) {
$this->assertInstanceOf(ProcessStartEvent::class, $event);
} elseif ($callCount === 2) {
$this->assertInstanceOf(ProcessErrorEvent::class, $event);
}

return $event;
});

$handler = new ProcessHandler($locator, $eventDispatcher);
$handler->__invoke('my_service::nonexistent', 'test_process');
// Should not throw - error is caught by try-catch and dispatched as event
}

public function testInvokeDispatchesErrorEventWhenServiceMethodThrowsException(): void
{
$service = new class {
public function run(): never
{
throw new \RuntimeException('Something went wrong');
}
};

$locator = $this->createMock(ContainerInterface::class);
$locator->method('get')->with('my_service')->willReturn($service);

$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
$eventDispatcher->expects($this->exactly(2))
->method('dispatch')
->willReturnCallback(function ($event) {
static $callCount = 0;
++$callCount;
if ($callCount === 1) {
$this->assertInstanceOf(ProcessStartEvent::class, $event);
} elseif ($callCount === 2) {
$this->assertInstanceOf(ProcessErrorEvent::class, $event);
$this->assertInstanceOf(\RuntimeException::class, $event->getError());
$this->assertSame('Something went wrong', $event->getError()->getMessage());
}

return $event;
});

$handler = new ProcessHandler($locator, $eventDispatcher);
$handler->__invoke('my_service::run', 'test_process');
}

/** @dataProvider provideInvalidServiceStrings */
public function testInvokeThrowsExceptionOnInvalidFormat(string $input): void
{
$locator = $this->createMock(ContainerInterface::class);
$eventDispatcher = $this->createMock(EventDispatcherInterface::class);

$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid service method format');

$handler = new ProcessHandler($locator, $eventDispatcher);
$handler->__invoke($input, 'test_process');
}

/** @return iterable<array{string}> */
public static function provideInvalidServiceStrings(): iterable
{
yield 'missing separator' => ['JustAService'];
yield 'empty service ID' => ['::method'];
yield 'empty method name' => ['service::'];
yield 'empty string' => [''];
}
}
Loading