diff --git a/.github/dependabot.yml b/.github/dependabot.yml index db86156..10f7e30 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,13 +4,17 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "daily" - # Too noisy. See https://github.community/t/increase-if-necessary-for-github-actions-in-dependabot/179581 - open-pull-requests-limit: 0 + interval: "weekly" + cooldown: + default-days: 7 + ignore: + - dependency-name: "yiisoft/*" # Maintain dependencies for Composer - package-ecosystem: "composer" directory: "/" schedule: interval: "daily" + cooldown: + default-days: 7 versioning-strategy: increase-if-necessary diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fd797cf..a375a6c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,12 +22,21 @@ on: name: build +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read jobs: phpunit: + name: phpunit runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0 + with: + persist-credentials: false - name: Build working-directory: ./tests run: docker compose build @@ -35,7 +44,7 @@ jobs: working-directory: ./tests run: docker compose run --rm php-cli vendor/bin/phpunit --coverage-clover ./tests/runtime/coverage.xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0 with: verbose: true files: ./tests/runtime/coverage.xml diff --git a/.github/workflows/composer-require-checker.yml b/.github/workflows/composer-require-checker.yml index a857bce..560a8c0 100644 --- a/.github/workflows/composer-require-checker.yml +++ b/.github/workflows/composer-require-checker.yml @@ -24,6 +24,8 @@ on: name: Composer require checker +permissions: + contents: read jobs: composer-require-checker: uses: yiisoft/actions/.github/workflows/composer-require-checker.yml@master diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index 4c64b99..98b3351 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -20,14 +20,21 @@ on: name: mutation test +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + permissions: contents: read jobs: mutation: + name: mutation runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - name: Mutation tests run: make mutation diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml index 7bb92d8..fd176e6 100644 --- a/.github/workflows/rector.yml +++ b/.github/workflows/rector.yml @@ -11,14 +11,21 @@ on: name: rector +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + permissions: contents: read jobs: rector: + name: rector runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - name: Rector run: make rector diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index e33eca8..2047aec 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -22,6 +22,8 @@ on: name: static analysis +permissions: + contents: read jobs: psalm: uses: yiisoft/actions/.github/workflows/psalm.yml@master diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000..430255d --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,22 @@ +name: GitHub Actions Security Analysis with zizmor 🌈 + +on: + push: + branches: + - master + - main + paths: + - '.github/**.yml' + - '.github/**.yaml' + pull_request: + paths: + - '.github/**.yml' + - '.github/**.yaml' + +permissions: + actions: read # Required by zizmor when reading workflow metadata through the API. + contents: read # Required to read workflow files. + +jobs: + zizmor: + uses: yiisoft/actions/.github/workflows/zizmor.yml@master diff --git a/src/Adapter.php b/src/Adapter.php index 0ce544c..707b5a0 100644 --- a/src/Adapter.php +++ b/src/Adapter.php @@ -9,7 +9,7 @@ use Yiisoft\Queue\Cli\LoopInterface; use Yiisoft\Queue\Message\IdEnvelope; use Yiisoft\Queue\Message\MessageInterface; -use Yiisoft\Queue\Message\MessageSerializerInterface; +use Yiisoft\Queue\Message\Serializer\MessageSerializerInterface; use Yiisoft\Queue\MessageStatus; final class Adapter implements AdapterInterface @@ -63,7 +63,7 @@ public function status(int|string $id): MessageStatus public function push(MessageInterface $message): MessageInterface { $payload = $this->serializer->serialize($message); - $id = $this->provider->pushMessage($payload, $message->getMetadata()); + $id = $this->provider->pushMessage($payload, $message->getMeta()); return new IdEnvelope($message, $id); } diff --git a/src/Message/Message.php b/src/Message/Message.php index 93c9491..f111420 100644 --- a/src/Message/Message.php +++ b/src/Message/Message.php @@ -6,14 +6,19 @@ use Yiisoft\Queue\Message\MessageInterface; +/** + * @psalm-import-type MessageMeta from MessageInterface + * @psalm-import-type MessagePayload from MessageInterface + */ final class Message implements MessageInterface { /** - * @param array $metadata + * @psalm-param MessagePayload $data + * @psalm-param MessageMeta $metadata */ public function __construct( private string $handlerName, - private mixed $data, + private bool|int|float|string|array|null $data, private array $metadata, private int $delay = 0 //delay in seconds ) { @@ -44,8 +49,13 @@ public function getData(): mixed return $this->data; } + public function getPayload(): bool|int|float|string|array|null + { + return $this->data; + } + /** - * @return array + * @psalm-return MessageMeta */ public function getMetadata(): array { @@ -53,7 +63,15 @@ public function getMetadata(): array } /** - * @param array $metadata + * @psalm-return MessageMeta + */ + public function getMeta(): array + { + return $this->metadata; + } + + /** + * @psalm-param MessageMeta $metadata */ public function withMetadata(array $metadata): static { @@ -62,8 +80,54 @@ public function withMetadata(array $metadata): static return $message; } + /** + * @psalm-param MessageMeta $meta + */ + public function withMeta(array $meta): static + { + return $this->withMetadata($meta); + } + public static function fromData(string $type, mixed $data): self { + self::assertPayload($data); return new self($type, $data, []); } + + public static function fromPayload(string $type, bool|int|float|string|array|null $payload): self + { + return new self($type, $payload, []); + } + + /** + * @psalm-assert MessagePayload $payload + */ + private static function assertPayload(mixed $payload): void + { + if (!self::isPayload($payload)) { + throw new \InvalidArgumentException('Payload must contain only null, scalar values, and arrays of them.'); + } + } + + /** + * @psalm-assert-if-true MessagePayload $payload + */ + private static function isPayload(mixed $payload): bool + { + if ($payload === null || is_scalar($payload)) { + return true; + } + + if (!is_array($payload)) { + return false; + } + + foreach ($payload as $value) { + if (!self::isPayload($value)) { + return false; + } + } + + return true; + } } diff --git a/src/QueueProvider.php b/src/QueueProvider.php index da397a8..e073a6e 100644 --- a/src/QueueProvider.php +++ b/src/QueueProvider.php @@ -26,6 +26,8 @@ public function __construct( } /** + * @param array $metadata + * * @throws RedisException */ public function pushMessage(string $message, array $metadata = []): int diff --git a/src/QueueProviderInterface.php b/src/QueueProviderInterface.php index 4efefd9..a57b94d 100644 --- a/src/QueueProviderInterface.php +++ b/src/QueueProviderInterface.php @@ -6,6 +6,9 @@ interface QueueProviderInterface { + /** + * @param array $metadata + */ public function pushMessage(string $message, array $metadata = []): int; /** diff --git a/tests/Integration/QueueProviderTest.php b/tests/Integration/QueueProviderTest.php index 09d2281..1b529b5 100644 --- a/tests/Integration/QueueProviderTest.php +++ b/tests/Integration/QueueProviderTest.php @@ -36,7 +36,7 @@ public function test__construct(): QueueProvider public function testDelay(QueueProvider $provider): void { $message = new Message('test', ['key' => 'value'], [], 2); - $id = $provider->pushMessage(json_encode($message->getData(), JSON_THROW_ON_ERROR), $message->getMetadata()); + $id = $provider->pushMessage(json_encode($message->getPayload(), JSON_THROW_ON_ERROR), $message->getMeta()); $this->assertGreaterThan(0, $id); $reserv = $provider->reserve($id); $this->assertNull($reserv); diff --git a/tests/Integration/QueueTest.php b/tests/Integration/QueueTest.php index 5683720..f3f6fb0 100644 --- a/tests/Integration/QueueTest.php +++ b/tests/Integration/QueueTest.php @@ -6,10 +6,11 @@ use Yiisoft\Queue\Adapter\AdapterInterface; use Yiisoft\Queue\Cli\LoopInterface; -use Yiisoft\Queue\Message\JsonMessageSerializer; use Yiisoft\Queue\Message\GenericMessage as Message; use Yiisoft\Queue\Message\MessageInterface; -use Yiisoft\Queue\Message\MessageSerializerInterface; +use Yiisoft\Queue\Message\Serializer\JsonMessageEncoder; +use Yiisoft\Queue\Message\Serializer\MessageSerializer; +use Yiisoft\Queue\Message\Serializer\MessageSerializerInterface; use Yiisoft\Queue\MessageStatus; use Yiisoft\Queue\Queue; use Yiisoft\Queue\Redis\Adapter; @@ -65,7 +66,7 @@ public function testStatus(): void $mockReserved = $this->createMock(QueueProviderInterface::class); $mockReserved->method('existInReserved')->willReturn(true); - $adapter = new Adapter($mockReserved, new JsonMessageSerializer(), $this->getLoop()); + $adapter = new Adapter($mockReserved, new MessageSerializer(new JsonMessageEncoder()), $this->getLoop()); $status = $adapter->status('1'); $this->assertEquals(MessageStatus::RESERVED, $status); @@ -83,7 +84,7 @@ public function testListen(): void $adapter = new Adapter( $queueProvider ->withChannelName('yii-queue'), - new JsonMessageSerializer(), + new MessageSerializer(new JsonMessageEncoder()), $mockLoop, ); $queue = $this->getDefaultQueue($adapter); @@ -131,7 +132,7 @@ public function testAdapterNullMessage() $adapter = new Adapter( $provider, - new JsonMessageSerializer(), + new MessageSerializer(new JsonMessageEncoder()), $mockLoop, ); $notUseHandler = true; diff --git a/tests/Support/ExtendedSimpleMessageHandler.php b/tests/Support/ExtendedSimpleMessageHandler.php index ef74fc9..1cc87ab 100644 --- a/tests/Support/ExtendedSimpleMessageHandler.php +++ b/tests/Support/ExtendedSimpleMessageHandler.php @@ -17,7 +17,7 @@ public function __construct(private FileHelper $fileHelper) public function handle(MessageInterface $message): void { - $data = $message->getData(); + $data = $message->getPayload(); if (null !== $data) { $this->fileHelper->put($data['file_name'], $data['payload']['time']); } diff --git a/tests/Support/IntegrationTestCase.php b/tests/Support/IntegrationTestCase.php index 7eb8b4f..062fcd6 100644 --- a/tests/Support/IntegrationTestCase.php +++ b/tests/Support/IntegrationTestCase.php @@ -11,8 +11,9 @@ use Yiisoft\Queue\Adapter\AdapterInterface; use Yiisoft\Queue\Cli\LoopInterface; use Yiisoft\Queue\Cli\SignalLoop; -use Yiisoft\Queue\Message\JsonMessageSerializer; use Yiisoft\Queue\Message\MessageInterface; +use Yiisoft\Queue\Message\Serializer\JsonMessageEncoder; +use Yiisoft\Queue\Message\Serializer\MessageSerializer; use Yiisoft\Queue\Middleware\CallableFactory; use Yiisoft\Queue\Middleware\Consume\ConsumeMiddlewareDispatcher; use Yiisoft\Queue\Middleware\Consume\ConsumeMiddlewareFactory; @@ -91,7 +92,7 @@ protected function getMessageHandlers(): array return [ 'ext-simple' => [new ExtendedSimpleMessageHandler(new FileHelper()), 'handle'], 'exception-listen' => static function (MessageInterface $message) { - $data = $message->getData(); + $data = $message->getPayload(); if (null !== $data) { throw new \RuntimeException((string) $data['payload']['time']); } @@ -134,7 +135,7 @@ protected function getAdapter(): AdapterInterface { return $this->adapter ??= new Adapter( $this->getQueueProvider(), - new JsonMessageSerializer(), + new MessageSerializer(new JsonMessageEncoder()), $this->getLoop(), ); } diff --git a/tests/Unit/Message/MessageTest.php b/tests/Unit/Message/MessageTest.php index c1c3681..60c5e4e 100644 --- a/tests/Unit/Message/MessageTest.php +++ b/tests/Unit/Message/MessageTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Unit\Message; +namespace Yiisoft\Queue\Redis\Tests\Unit\Message; use PHPUnit\Framework\TestCase; use Yiisoft\Queue\Redis\Message\Message; -class MessageTest extends TestCase +final class MessageTest extends TestCase { public function testGetHandlerName(): void { @@ -20,6 +20,7 @@ public function testGetData(): void { $message = new Message('handler', 'data', []); $this->assertEquals('data', $message->getData()); + $this->assertEquals('data', $message->getPayload()); } public function testGetMetadata(): void @@ -27,10 +28,12 @@ public function testGetMetadata(): void $metadata = ['key' => 'value']; $message = new Message('handler', 'data', $metadata); $this->assertEquals($metadata, $message->getMetadata()); + $this->assertEquals($metadata, $message->getMeta()); $message = new Message('handler', 'data', $metadata, 2); $metadata['delay'] = 2; $this->assertEquals($metadata, $message->getMetadata()); + $this->assertEquals($metadata, $message->getMeta()); } public function testWithMetadata(): void @@ -43,6 +46,16 @@ public function testWithMetadata(): void $this->assertSame(['key' => 'value'], $messageWithMetadata->getMetadata()); } + public function testWithMeta(): void + { + $message = new Message('handler', 'data', []); + $messageWithMeta = $message->withMeta(['key' => 'value']); + + $this->assertNotSame($message, $messageWithMeta); + $this->assertSame([], $message->getMeta()); + $this->assertSame(['key' => 'value'], $messageWithMeta->getMeta()); + } + public function testWithDelay(): void { $message = new Message('handler', 'data', []); @@ -64,4 +77,32 @@ public function testFromData(): void $this->assertEquals($data, $message->getData()); $this->assertEquals([], $message->getMetadata()); } + + public function testFromPayload(): void + { + $handlerName = 'test-handler'; + $payload = ['key' => 'value']; + + $message = Message::fromPayload($handlerName, $payload); + + $this->assertSame($handlerName, $message->getType()); + $this->assertSame($payload, $message->getPayload()); + $this->assertSame([], $message->getMeta()); + } + + public function testFromDataFailsWithInvalidPayload(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Payload must contain only null, scalar values, and arrays of them.'); + + Message::fromData('handler', new \stdClass()); + } + + public function testFromDataFailsWithNestedInvalidPayload(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Payload must contain only null, scalar values, and arrays of them.'); + + Message::fromData('handler', ['nested' => new \stdClass()]); + } } diff --git a/tests/Unit/QueueTest.php b/tests/Unit/QueueTest.php index abbee71..642460e 100644 --- a/tests/Unit/QueueTest.php +++ b/tests/Unit/QueueTest.php @@ -6,10 +6,11 @@ use PHPUnit\Framework\TestCase; use Yiisoft\Queue\Cli\LoopInterface; -use Yiisoft\Queue\Message\JsonMessageSerializer; use Yiisoft\Queue\Message\Message; use Yiisoft\Queue\Message\MessageInterface; -use Yiisoft\Queue\Message\MessageSerializerInterface; +use Yiisoft\Queue\Message\Serializer\JsonMessageEncoder; +use Yiisoft\Queue\Message\Serializer\MessageSerializer; +use Yiisoft\Queue\Message\Serializer\MessageSerializerInterface; use Yiisoft\Queue\Redis\Adapter; use Yiisoft\Queue\Redis\QueueProviderInterface; @@ -37,7 +38,7 @@ public function testAdapterNullMessage() $adapter = new Adapter( $provider, - new JsonMessageSerializer(), + new MessageSerializer(new JsonMessageEncoder()), $mockLoop, ); $notUseHandler = true;