Skip to content

Commit 4d6269d

Browse files
committed
fix(faker): implement path parameter injection and resolve data generation issues
- Implement FakerContext to propagate path parameters from request to fakers - Ensure ObjectFaker uses path parameter values when they match property names - Fix NumberFaker to default to non-negative values for IDs/integers - Fix ObjectFaker bug where it could return an empty object for schemas without required fields - Update all fakers and unit tests to support the new FakerContext - Ensure RequestValidator correctly extracts path parameters even when validation is disabled - Fix PHPStan and PHP-CS-Fixer issues in fakers and tests
1 parent 1f82315 commit 4d6269d

24 files changed

Lines changed: 628 additions & 404 deletions

src/Middleware/MockMiddleware/Faker/OpenAPIFaker.php

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Faker\Exception\NoRequest;
1919
use WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Faker\Exception\NoResponse;
2020
use WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Faker\Exception\NoSchema;
21+
use WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Faker\SchemaFaker\FakerContext;
2122
use WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Faker\SchemaFaker\FakerRegistry;
2223
use WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Utils\HttpMethod;
2324
use Webmozart\Assert\Assert;
@@ -71,56 +72,94 @@ public static function createFromSchema(OpenApi $openApi): self
7172
return $instance;
7273
}
7374

75+
/**
76+
* @param array<string, mixed> $pathParameters
77+
*/
7478
public function mockRequest(
7579
string $path,
7680
string $method,
7781
string $contentType = 'application/json',
82+
array $pathParameters = []
7883
): mixed {
7984
$mediaType = $this->findContentForRequest($path, HttpMethod::fromString($method), $contentType);
8085

81-
return $this->fakerRegistry->getRequestFaker()->generate($mediaType, $this->options, $this->fakerRegistry);
86+
return $this->fakerRegistry->getRequestFaker()->generate(
87+
$mediaType,
88+
$this->options,
89+
$this->fakerRegistry,
90+
FakerContext::request($pathParameters)
91+
);
8292
}
8393

94+
/**
95+
* @param array<string, mixed> $pathParameters
96+
*/
8497
public function mockRequestForExample(
8598
string $path,
8699
string $method,
87100
string $exampleName,
88101
string $contentType = 'application/json',
102+
array $pathParameters = []
89103
): mixed {
90104
$mediaType = $this->findContentForRequest($path, HttpMethod::fromString($method), $contentType);
91105

92-
return $this->fakerRegistry->getRequestFaker()->generate($mediaType, $this->options, $this->fakerRegistry, $exampleName);
106+
return $this->fakerRegistry->getRequestFaker()->generate(
107+
$mediaType,
108+
$this->options,
109+
$this->fakerRegistry,
110+
FakerContext::request($pathParameters),
111+
$exampleName
112+
);
93113
}
94114

115+
/**
116+
* @param array<string, mixed> $pathParameters
117+
*/
95118
public function mockResponse(
96119
string $path,
97120
string $method,
98121
string $statusCode = '200',
99122
string $contentType = 'application/json',
123+
array $pathParameters = []
100124
): mixed {
101125
$mediaType = $this->findContentForResponse($path, HttpMethod::fromString($method), $statusCode, $contentType);
102126

103-
return $this->fakerRegistry->getResponseFaker()->generate($mediaType, $this->options, $this->fakerRegistry);
127+
return $this->fakerRegistry->getResponseFaker()->generate(
128+
$mediaType,
129+
$this->options,
130+
$this->fakerRegistry,
131+
FakerContext::response($pathParameters)
132+
);
104133
}
105134

135+
/**
136+
* @param array<string, mixed> $pathParameters
137+
*/
106138
public function mockResponseForExample(
107139
string $path,
108140
string $method,
109141
string $exampleName,
110142
string $statusCode = '200',
111143
string $contentType = 'application/json',
144+
array $pathParameters = []
112145
): mixed {
113146
$mediaType = $this->findContentForResponse($path, HttpMethod::fromString($method), $statusCode, $contentType);
114147

115-
return $this->fakerRegistry->getResponseFaker()->generate($mediaType, $this->options, $this->fakerRegistry, $exampleName);
148+
return $this->fakerRegistry->getResponseFaker()->generate(
149+
$mediaType,
150+
$this->options,
151+
$this->fakerRegistry,
152+
FakerContext::response($pathParameters),
153+
$exampleName
154+
);
116155
}
117156

118157
/** @throws NoSchema */
119158
public function mockComponentSchema(string $schemaName): mixed
120159
{
121160
$schema = $this->findComponentSchema($schemaName);
122161

123-
return $this->fakerRegistry->getSchemaFaker()->generate($schema, $this->options);
162+
return $this->fakerRegistry->getSchemaFaker()->generate($schema, $this->options, FakerContext::response());
124163
}
125164

126165
/** @throws NoSchema */

src/Middleware/MockMiddleware/Faker/SchemaFaker/ArrayFaker.php

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,22 @@
44

55
namespace WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Faker\SchemaFaker;
66

7+
use cebe\openapi\spec\Reference;
78
use cebe\openapi\spec\Schema;
8-
use Faker\Provider\Base;
99
use WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Faker\MockStrategy;
1010
use WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Faker\Options;
1111
use Webmozart\Assert\Assert;
1212

13+
use function array_map;
1314
use function array_unique;
1415
use function count;
15-
16-
use const SORT_REGULAR;
16+
use function range;
1717

1818
/** @internal */
1919
final class ArrayFaker implements FakerInterface
2020
{
21-
/** @return array<mixed> */
22-
public function generate(Schema $schema, Options $options, FakerRegistry $fakerRegistry): array
21+
/** @return array<mixed>|null */
22+
public function generate(Schema $schema, Options $options, FakerRegistry $fakerRegistry, FakerContext $fakerContext): ?array
2323
{
2424
$useStaticStrategy = $options->getStrategy() === MockStrategy::STATIC;
2525

@@ -31,45 +31,47 @@ public function generate(Schema $schema, Options $options, FakerRegistry $fakerR
3131
if ($schema->default !== null) {
3232
return (array) $schema->default;
3333
}
34+
35+
if ($schema->nullable) {
36+
return null;
37+
}
3438
}
3539

36-
$minimum = $schema->minItems ?? ($useStaticStrategy ? 1 : 0);
37-
$maximum = $schema->maxItems ?? ($useStaticStrategy ? $minimum : 10);
40+
$minItems = $schema->minItems ?? ($useStaticStrategy ? 1 : 0);
41+
$maxItems = $schema->maxItems ?? ($useStaticStrategy ? $minItems : 10);
3842

39-
if ($options->getMinItems() && $minimum < $options->getMinItems()) {
40-
$minimum = $options->getMinItems();
43+
if ($options->getMinItems() !== null && $minItems < $options->getMinItems()) {
44+
$minItems = $options->getMinItems();
4145
}
4246

43-
if ($options->getMaxItems() && $maximum > $options->getMaxItems()) {
44-
$maximum = $options->getMaxItems();
47+
if ($options->getMaxItems() !== null && $maxItems > $options->getMaxItems()) {
48+
$maxItems = $options->getMaxItems();
4549

46-
if ($minimum > $maximum) {
47-
$minimum = $maximum;
50+
if ($minItems > $maxItems) {
51+
$minItems = $maxItems;
4852
}
4953
}
5054

51-
$itemSize = $useStaticStrategy ? $minimum : Base::numberBetween($minimum, $maximum);
52-
53-
$fakeData = [];
54-
$itemSchema = $schema->items;
55-
Assert::isInstanceOf($itemSchema, Schema::class);
55+
$count = $useStaticStrategy ? $minItems : random_int($minItems, $maxItems);
5656

57-
for ($i = 0; $i < $itemSize; ++$i) {
58-
$fakeData[] = $fakerRegistry->generate($itemSchema, $options);
57+
$itemsSchema = $schema->items;
58+
if ($itemsSchema instanceof Reference) {
59+
$itemsSchema = $itemsSchema->resolve();
60+
}
5961

60-
if (! $schema->uniqueItems) {
61-
continue;
62-
}
62+
Assert::isInstanceOf($itemsSchema, Schema::class);
6363

64-
/** @var array<int, string|int|float|bool|array<mixed>|null> $fakeData */
65-
$uniqueData = array_unique($fakeData, SORT_REGULAR);
64+
$fakeData = array_map(
65+
fn (): mixed => $fakerRegistry->generate($itemsSchema, $options, $fakerContext),
66+
$count > 0 ? range(1, $count) : []
67+
);
6668

67-
if (count($uniqueData) > count($fakeData)) {
68-
continue;
69+
if ($schema->uniqueItems ?? false) {
70+
$fakeData = array_unique($fakeData, SORT_REGULAR);
71+
while (count($fakeData) < $count) {
72+
$fakeData[] = $fakerRegistry->generate($itemsSchema, $options, $fakerContext);
73+
$fakeData = array_unique($fakeData, SORT_REGULAR);
6974
}
70-
71-
$i -= count($fakeData) - count($uniqueData);
72-
$fakeData = $uniqueData;
7375
}
7476

7577
return $fakeData;

src/Middleware/MockMiddleware/Faker/SchemaFaker/BooleanFaker.php

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,12 @@
99
use WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Faker\MockStrategy;
1010
use WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Faker\Options;
1111

12-
use function random_int;
1312
use function reset;
1413

1514
/** @internal */
1615
final class BooleanFaker implements FakerInterface
1716
{
18-
public function generate(Schema $schema, Options $options, FakerRegistry $fakerRegistry): bool|null
17+
public function generate(Schema $schema, Options $options, FakerRegistry $fakerRegistry, FakerContext $fakerContext): ?bool
1918
{
2019
if ($options->getStrategy() === MockStrategy::STATIC) {
2120
return $this->generateStatic($schema);
@@ -27,15 +26,18 @@ public function generate(Schema $schema, Options $options, FakerRegistry $fakerR
2726
private function generateDynamic(Schema $schema): bool
2827
{
2928
if (! empty($schema->enum)) {
30-
return (bool) Base::randomElement($schema->enum);
29+
/** @var bool $value */
30+
$value = Base::randomElement($schema->enum);
31+
32+
return $value;
3133
}
3234

33-
return random_int(0, 1) < 0.5;
35+
return Base::randomElement([true, false]);
3436
}
3537

36-
private function generateStatic(Schema $schema): bool|null
38+
private function generateStatic(Schema $schema): ?bool
3739
{
38-
if (! empty($schema->default)) {
40+
if ($schema->default !== null) {
3941
return (bool) $schema->default;
4042
}
4143

@@ -48,6 +50,7 @@ private function generateStatic(Schema $schema): bool|null
4850
}
4951

5052
if (! empty($schema->enum)) {
53+
/** @var array<bool> $enums */
5154
$enums = $schema->enum;
5255

5356
return (bool) reset($enums);

src/Middleware/MockMiddleware/Faker/SchemaFaker/FakerContext.php

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,51 @@
55
namespace WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Faker\SchemaFaker;
66

77
/** @internal */
8-
enum FakerContext: string
8+
final readonly class FakerContext
99
{
10-
case REQUEST = 'request';
11-
case RESPONSE = 'response';
10+
public const string REQUEST = 'request';
11+
public const string RESPONSE = 'response';
12+
13+
/**
14+
* @param array<string, mixed> $pathParameters
15+
*/
16+
public function __construct(
17+
private string $context = self::RESPONSE,
18+
private array $pathParameters = []
19+
) {
20+
}
21+
22+
/**
23+
* @param array<string, mixed> $pathParameters
24+
*/
25+
public static function request(array $pathParameters = []): self
26+
{
27+
return new self(self::REQUEST, $pathParameters);
28+
}
29+
30+
/**
31+
* @param array<string, mixed> $pathParameters
32+
*/
33+
public static function response(array $pathParameters = []): self
34+
{
35+
return new self(self::RESPONSE, $pathParameters);
36+
}
1237

1338
public function isRequest(): bool
1439
{
15-
return $this === self::REQUEST;
40+
return $this->context === self::REQUEST;
41+
}
42+
43+
public function getContext(): string
44+
{
45+
return $this->context;
46+
}
47+
48+
/**
49+
* @return array<string, mixed>
50+
*/
51+
public function getPathParameters(): array
52+
{
53+
return $this->pathParameters;
1654
}
1755
}

src/Middleware/MockMiddleware/Faker/SchemaFaker/FakerInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@
1010
/** @internal */
1111
interface FakerInterface
1212
{
13-
public function generate(Schema $schema, Options $options, FakerRegistry $fakerRegistry): mixed;
13+
public function generate(Schema $schema, Options $options, FakerRegistry $fakerRegistry, FakerContext $fakerContext): mixed;
1414
}

src/Middleware/MockMiddleware/Faker/SchemaFaker/FakerRegistry.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
use cebe\openapi\spec\Schema;
88
use WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Faker\Options;
99

10+
use function is_array;
11+
use function is_string;
12+
use function reset;
13+
1014
/** @internal */
1115
final class FakerRegistry
1216
{
@@ -29,19 +33,15 @@ public function __construct()
2933
];
3034
}
3135

32-
public function generate(Schema $schema, Options $options, FakerContext $fakerContext = FakerContext::RESPONSE): mixed
36+
public function generate(Schema $schema, Options $options, FakerContext $fakerContext): mixed
3337
{
3438
$fakerType = $this->resolveType($schema);
3539

3640
if (isset($this->fakers[$fakerType->value])) {
3741
$faker = $this->fakers[$fakerType->value];
3842

39-
if ($faker instanceof ObjectFaker) {
40-
return $faker->generate($schema, $options, $this, $fakerContext);
41-
}
42-
4343
if ($faker instanceof FakerInterface) {
44-
return $faker->generate($schema, $options, $this);
44+
return $faker->generate($schema, $options, $this, $fakerContext);
4545
}
4646
}
4747

0 commit comments

Comments
 (0)