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
24 changes: 24 additions & 0 deletions .github/workflows/rector.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
on:
pull_request_target:
paths-ignore:
- 'docs/**'
- 'README.md'
- 'CHANGELOG.md'
- '.gitignore'
- '.gitattributes'
- 'infection.json.dist'
- 'psalm.xml'

name: rector

jobs:
rector:
uses: yiisoft/actions/.github/workflows/rector.yml@master
secrets:
token: ${{ secrets.YIISOFT_GITHUB_TOKEN }}
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
os: >-
['ubuntu-latest']
php: >-
['8.4']
Comment on lines +16 to +24

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {}

Copilot Autofix

AI 5 months ago

To fix this problem, the workflow should explicitly set the permissions block at the workflow or job level, specifying the minimum required access for the workflow to function. In the most secure configuration, set permissions: contents: read at the root, or within the specific job if different jobs have different needs. Because this workflow largely delegates to an external reusable workflow (via uses), the safest generic policy is to provide only read access unless a more specific write access is proven necessary for this action to work. The recommended best practice is to add a permissions block at the job level (right under the rector job—next to uses and with). By doing so, we minimize the token’s permissions for this job only, and can later expand them if downstream linter/rector steps require it.

To implement the fix:

  • Insert the following block directly after the job name (rector:) or before uses:
    permissions:
      contents: read
  • If future requirements necessitate additional permissions (e.g., pull-requests: write), they can be added as new lines under permissions.

Suggested changeset 1
.github/workflows/rector.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml
--- a/.github/workflows/rector.yml
+++ b/.github/workflows/rector.yml
@@ -13,6 +13,8 @@
 
 jobs:
   rector:
+    permissions:
+      contents: read
     uses: yiisoft/actions/.github/workflows/rector.yml@master
     secrets:
       token: ${{ secrets.YIISOFT_GITHUB_TOKEN }}
EOF
@@ -13,6 +13,8 @@

jobs:
rector:
permissions:
contents: read
uses: yiisoft/actions/.github/workflows/rector.yml@master
secrets:
token: ${{ secrets.YIISOFT_GITHUB_TOKEN }}
Copilot is powered by AI and may make mistakes. Always verify output.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
- Enh #44: Add psalm type `int<1, 2147483647>` to `depth` parameter in `JsonParser` constructor (@vjik)
- Bug #44: Explicitly mark nullable parameters (@vjik)
- Bug #46: Explicitly add transitive dependency `psr/http-factory` (@vjik)
- Enh #47: Use promoted readonly properties in `JsonParser`, `BadRequestHandler` and `RequestBodyParser` classes (@vjik)
- Enh #47: Minor refactor `RequestBodyParser`: use `str_contains()` function instead of `strpos()` and `::class` instead
of `get_class()` (@vjik)

## 1.1.1 June 03, 2024

Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"httpsoft/http-message": "^1.1.6",
"maglnet/composer-require-checker": "^4.7.1",
"phpunit/phpunit": "^10.5.55",
"rector/rector": "^2.1",
"roave/infection-static-analysis-plugin": "^1.35",
"spatie/phpunit-watcher": "^1.24",
"vimeo/psalm": "^5.26.1 || ^6.13.1",
Expand Down
18 changes: 18 additions & 0 deletions docs/internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,21 @@ The code is statically analyzed with [Psalm](https://psalm.dev/). To run static
```shell
./vendor/bin/psalm
```

## Code style

Use [Rector](https://github.com/rectorphp/rector) to make codebase follow some specific rules or
use either newest or any specific version of PHP:

```shell
./vendor/bin/rector
```

## Dependencies

Use [ComposerRequireChecker](https://github.com/maglnet/ComposerRequireChecker) to detect transitive
[Composer](https://getcomposer.org) dependencies:

```shell
./vendor/bin/composer-require-checker
```
24 changes: 24 additions & 0 deletions rector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

use Rector\CodeQuality\Rector\Class_\InlineConstructorDefaultToPropertyRector;
use Rector\Config\RectorConfig;
use Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector;
use Rector\Php81\Rector\Property\ReadOnlyPropertyRector;
use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector;

return RectorConfig::configure()
->withPaths([
__DIR__ . '/src',
__DIR__ . '/tests',
])
->withPhpSets(php81: true)
->withRules([
InlineConstructorDefaultToPropertyRector::class,
])
->withSkip([
ClosureToArrowFunctionRector::class,
ReadOnlyPropertyRector::class,
NullToStrictStringFuncCallArgRector::class,
]);
7 changes: 3 additions & 4 deletions src/BadRequestHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@
*/
final class BadRequestHandler implements BadRequestHandlerInterface
{
private ResponseFactoryInterface $responseFactory;
private ?ParserException $parserException = null;

public function __construct(ResponseFactoryInterface $responseFactory)
{
$this->responseFactory = $responseFactory;
public function __construct(
private readonly ResponseFactoryInterface $responseFactory,
) {
}

public function handle(ServerRequestInterface $request): ResponseInterface
Expand All @@ -40,7 +39,7 @@

public function withParserException(ParserException $e): BadRequestHandlerInterface
{
$new = clone $this;

Check warning on line 42 in src/BadRequestHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "CloneRemoval": @@ @@ } public function withParserException(ParserException $e): BadRequestHandlerInterface { - $new = clone $this; + $new = $this; $new->parserException = $e; return $new; } }
$new->parserException = $e;
return $new;
}
Expand Down
14 changes: 3 additions & 11 deletions src/Parser/JsonParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,6 @@
*/
final class JsonParser implements ParserInterface
{
private bool $convertToAssociativeArray;
/** @psalm-var int<1, 2147483647> */
private int $depth;
private int $options;

/**
* @param bool $convertToAssociativeArray Whether objects should be converted to associative array during parsing.
* @param int $depth Maximum JSON recursion depth.
Expand All @@ -30,13 +25,10 @@
* @psalm-param int<1, 2147483647> $depth
*/
public function __construct(
bool $convertToAssociativeArray = true,
int $depth = 512,
int $options = JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE
private readonly bool $convertToAssociativeArray = true,
private readonly int $depth = 512,

Check warning on line 29 in src/Parser/JsonParser.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "IncrementInteger": @@ @@ * * @psalm-param int<1, 2147483647> $depth */ - public function __construct(private readonly bool $convertToAssociativeArray = true, private readonly int $depth = 512, private readonly int $options = JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE) + public function __construct(private readonly bool $convertToAssociativeArray = true, private readonly int $depth = 513, private readonly int $options = JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE) { } public function parse(string $rawBody)

Check warning on line 29 in src/Parser/JsonParser.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "DecrementInteger": @@ @@ * * @psalm-param int<1, 2147483647> $depth */ - public function __construct(private readonly bool $convertToAssociativeArray = true, private readonly int $depth = 512, private readonly int $options = JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE) + public function __construct(private readonly bool $convertToAssociativeArray = true, private readonly int $depth = 511, private readonly int $options = JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE) { } public function parse(string $rawBody)
private readonly int $options = JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE,
) {
$this->convertToAssociativeArray = $convertToAssociativeArray;
$this->depth = $depth;
$this->options = $options;
}

public function parse(string $rawBody)
Expand Down
9 changes: 3 additions & 6 deletions src/RequestBodyParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
use Yiisoft\Request\Body\Parser\JsonParser;

use function array_key_exists;
use function get_class;
use function is_array;
use function is_object;

Expand All @@ -29,7 +28,6 @@
*/
final class RequestBodyParser implements MiddlewareInterface
{
private ContainerInterface $container;
private BadRequestHandlerInterface $badRequestHandler;

/**
Expand All @@ -43,10 +41,9 @@

public function __construct(
ResponseFactoryInterface $responseFactory,
ContainerInterface $container,
private readonly ContainerInterface $container,
BadRequestHandlerInterface|null $badRequestHandler = null
) {
$this->container = $container;
$this->badRequestHandler = $badRequestHandler ?? new BadRequestHandler($responseFactory);
}

Expand Down Expand Up @@ -89,7 +86,7 @@
return $new;
}
foreach ($mimeTypes as $mimeType) {
$this->validateMimeType($mimeType);

Check warning on line 89 in src/RequestBodyParser.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "MethodCallRemoval": @@ @@ return $new; } foreach ($mimeTypes as $mimeType) { - $this->validateMimeType($mimeType); + unset($new->parsers[$this->normalizeMimeType($mimeType)]); } return $new;
unset($new->parsers[$this->normalizeMimeType($mimeType)]);
}
return $new;
Expand All @@ -115,7 +112,7 @@
/** @var mixed $parsed */
$parsed = $parser->parse((string)$request->getBody());
if ($parsed !== null && !is_object($parsed) && !is_array($parsed)) {
$parserClass = get_class($parser);
$parserClass = $parser::class;
throw new RuntimeException(
"$parserClass::parse() return value must be an array, an object, or null."
);
Expand Down Expand Up @@ -147,12 +144,12 @@
private function getContentType(ServerRequestInterface $request): ?string
{
$contentType = $request->getHeaderLine(Header::CONTENT_TYPE);
if (trim($contentType) !== '') {

Check warning on line 147 in src/RequestBodyParser.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "UnwrapTrim": @@ @@ private function getContentType(ServerRequestInterface $request): ?string { $contentType = $request->getHeaderLine(Header::CONTENT_TYPE); - if (trim($contentType) !== '') { + if ($contentType !== '') { if (str_contains($contentType, ';')) { $contentTypeParts = explode(';', $contentType, 2); return strtolower(trim($contentTypeParts[0]));
if (str_contains($contentType, ';')) {
$contentTypeParts = explode(';', $contentType, 2);
return strtolower(trim($contentTypeParts[0]));
}
return strtolower(trim($contentType));

Check warning on line 152 in src/RequestBodyParser.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "UnwrapTrim": @@ @@ $contentTypeParts = explode(';', $contentType, 2); return strtolower(trim($contentTypeParts[0])); } - return strtolower(trim($contentType)); + return strtolower($contentType); } return null; }
}
return null;
}
Expand All @@ -162,7 +159,7 @@
*/
private function validateMimeType(string $mimeType): void
{
if (strpos($mimeType, '/') === false) {
if (!str_contains($mimeType, '/')) {
throw new InvalidArgumentException('Invalid mime type.');
}
}
Expand Down
11 changes: 4 additions & 7 deletions tests/MockParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,10 @@

final class MockParser implements ParserInterface
{
private $response;
private bool $throwException;

public function __construct($response, bool $throwException)
{
$this->response = $response;
$this->throwException = $throwException;
public function __construct(
private readonly array|object|null $response,
private readonly bool $throwException,
) {
}

public function parse(string $rawBody)
Expand Down
42 changes: 18 additions & 24 deletions tests/RequestBodyParsersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public function testWithParser(): void
public function testWithParserWithEmptyParserClass(): void
{
$containerId = 'testParser';
$container = $this->getContainerWithParser($containerId, '');
$container = $this->getContainerWithParser($containerId);

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The parser class cannot be an empty string.');
Expand Down Expand Up @@ -101,7 +101,7 @@ public function testWithBadRequestResponse(): void
public function testWithoutBadRequestResponse(): void
{
$containerId = 'test';
$container = $this->getContainerWithParser($containerId, '', true);
$container = $this->getContainerWithParser($containerId, throwException: true);
$mimeType = 'test/test';
$bodyParser = $this
->getRequestBodyParser($container)
Expand Down Expand Up @@ -144,7 +144,7 @@ public function testThrownExceptionWithNotExistsParser(): void
public function testThrownExceptionWithInvalidMimeType(): void
{
$containerId = 'testParser';
$container = $this->getContainerWithParser($containerId, '');
$container = $this->getContainerWithParser($containerId);

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid mime type.');
Expand Down Expand Up @@ -222,8 +222,11 @@ private function getContainerWithResponseFactory(): SimpleContainer
);
}

private function getContainerWithParser(string $id, $expectedOutput, bool $throwException = false): SimpleContainer
{
private function getContainerWithParser(
string $id,
array|object|null $expectedOutput = null,
bool $throwException = false,
): SimpleContainer {
return new SimpleContainer(
[
ResponseFactoryInterface::class => $this->createMock(ResponseFactoryInterface::class),
Expand All @@ -250,12 +253,11 @@ private function createHandler(): BadRequestHandlerInterface
->willReturn(Status::OK);

return new class ($mockResponse) implements BadRequestHandlerInterface {
private $requestParsedBody;
private ResponseInterface $mockResponse;
private array|object|null $requestParsedBody;

public function __construct(ResponseInterface $mockResponse)
{
$this->mockResponse = $mockResponse;
public function __construct(
private readonly ResponseInterface $mockResponse,
) {
}

public function handle(ServerRequestInterface $request): ResponseInterface
Expand All @@ -264,10 +266,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface
return $this->mockResponse;
}

/**
* @return array|object|null
*/
public function getRequestParsedBody()
public function getRequestParsedBody(): array|object|null
{
return $this->requestParsedBody;
}
Expand All @@ -290,21 +289,16 @@ private function getRequestBodyParser(
private function createCustomBadResponseHandler(string $body): BadRequestHandlerInterface
{
return new class ($body, new ResponseFactory()) implements BadRequestHandlerInterface {
private string $body;
private ResponseFactoryInterface $responseFactory;

public function __construct(string $body, ResponseFactoryInterface $responseFactory)
{
$this->body = $body;
$this->responseFactory = $responseFactory;
public function __construct(
private readonly string $body,
private readonly ResponseFactoryInterface $responseFactory
) {
}

public function handle(ServerRequestInterface $request): ResponseInterface
{
$response = $this->responseFactory->createResponse(Status::BAD_REQUEST);
$response
->getBody()
->write($this->body);
$response->getBody()->write($this->body);
return $response;
}

Expand Down
Loading