Skip to content

Commit 5f97eb8

Browse files
committed
feat: implement faker testing support
1 parent 14b5a34 commit 5f97eb8

14 files changed

Lines changed: 737 additions & 0 deletions
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Constructo\Testing\Extension;
6+
7+
use Constructo\Core\Serialize\Builder;
8+
9+
trait BuilderExtension
10+
{
11+
private ?Builder $builder = null;
12+
13+
protected function builder(): Builder
14+
{
15+
if ($this->builder === null) {
16+
$this->builder = $this->make(Builder::class);
17+
}
18+
return $this->builder;
19+
}
20+
21+
/**
22+
* @template T of mixed
23+
* @param class-string<T> $class
24+
* @param array<string, mixed> $args
25+
*
26+
* @return T
27+
*/
28+
abstract protected function make(string $class, array $args = []): mixed;
29+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Constructo\Testing\Extension;
6+
7+
use Constructo\Testing\Faker\Faker;
8+
use Faker\Generator;
9+
10+
trait FakerExtension
11+
{
12+
private ?Faker $faker = null;
13+
14+
protected function faker(): Faker
15+
{
16+
if ($this->faker === null) {
17+
$this->faker = $this->make(Faker::class);
18+
}
19+
return $this->faker;
20+
}
21+
22+
protected function generator(): Generator
23+
{
24+
return $this->faker()->generator();
25+
}
26+
27+
/**
28+
* @template T of mixed
29+
* @param class-string<T> $class
30+
* @param array<string, mixed> $args
31+
*
32+
* @return T
33+
*/
34+
abstract protected function make(string $class, array $args = []): mixed;
35+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Constructo\Testing\Extension;
6+
7+
use Constructo\Support\Reflective\Factory\Target;
8+
use ReflectionClass;
9+
use ReflectionException;
10+
11+
/**
12+
* @phpstan-ignore trait.unused
13+
*/
14+
trait MakeExtension
15+
{
16+
/**
17+
* @template T of mixed
18+
* @param class-string<T> $class
19+
* @param array<string, mixed> $args
20+
*
21+
* @return T
22+
* @throws ReflectionException
23+
*/
24+
protected function make(string $class, array $args = []): mixed
25+
{
26+
$target = Target::createFrom($class);
27+
return (new ReflectionClass($class))->newInstanceArgs(array_values($args));
28+
}
29+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Constructo\Testing\Extension;
6+
7+
use Constructo\Support\Managed;
8+
9+
trait ManagedExtension
10+
{
11+
private ?Managed $managed = null;
12+
13+
protected function managed(): Managed
14+
{
15+
if ($this->managed === null) {
16+
$this->managed = $this->make(class: Managed::class);
17+
}
18+
return $this->managed;
19+
}
20+
21+
/**
22+
* @template T of mixed
23+
* @param class-string<T> $class
24+
* @param array<string, mixed> $args
25+
*
26+
* @return T
27+
*/
28+
abstract protected function make(string $class, array $args = []): mixed;
29+
}

src/Testing/Faker/Faker.php

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Constructo\Testing\Faker;
6+
7+
use Constructo\Contract\Formatter;
8+
use Constructo\Contract\Testing\Faker as Contract;
9+
use Constructo\Support\Reflective\Engine;
10+
use Constructo\Support\Reflective\Factory\Target;
11+
use Constructo\Support\Reflective\Notation;
12+
use Constructo\Support\Set;
13+
use Constructo\Testing\Faker\Resolver\FromCollection;
14+
use Constructo\Testing\Faker\Resolver\FromDefaultValue;
15+
use Constructo\Testing\Faker\Resolver\FromDependency;
16+
use Constructo\Testing\Faker\Resolver\FromEnum;
17+
use Constructo\Testing\Faker\Resolver\FromPreset;
18+
use Constructo\Testing\Faker\Resolver\FromTypeAttributes;
19+
use Constructo\Testing\Faker\Resolver\FromTypeBuiltin;
20+
use Constructo\Testing\Faker\Resolver\FromTypeDate;
21+
use Faker\Factory;
22+
use Faker\Generator;
23+
use ReflectionException;
24+
use ReflectionParameter;
25+
26+
use function Constructo\Cast\stringify;
27+
use function getenv;
28+
29+
30+
class Faker extends Engine implements Contract
31+
{
32+
protected readonly Generator $generator;
33+
34+
/**
35+
* @param array<callable|Formatter> $formatters
36+
* @SuppressWarnings(StaticAccess)
37+
*/
38+
public function __construct(
39+
Notation $case = Notation::SNAKE,
40+
array $formatters = [],
41+
?string $locale = null,
42+
) {
43+
parent::__construct($case, $formatters);
44+
45+
$this->generator = Factory::create($this->locale($locale));
46+
}
47+
48+
public function __call(string $name, array $arguments): mixed
49+
{
50+
return $this->generate($name, $arguments);
51+
}
52+
53+
/**
54+
* @template U of object
55+
* @param class-string<U> $class
56+
* @throws ReflectionException
57+
*/
58+
public function fake(string $class, array $presets = []): Set
59+
{
60+
$target = Target::createFrom($class);
61+
$parameters = $target->getReflectionParameters();
62+
if (empty($parameters)) {
63+
return Set::createFrom([]);
64+
}
65+
66+
return $this->resolveParameters($parameters, new Set($presets));
67+
}
68+
69+
public function generate(string $name, array $arguments = []): mixed
70+
{
71+
return $this->generator->__call($name, $arguments);
72+
}
73+
74+
public function generator(): Generator
75+
{
76+
return $this->generator;
77+
}
78+
79+
/**
80+
* @param array<ReflectionParameter> $parameters
81+
*/
82+
private function resolveParameters(array $parameters, Set $presets): Set
83+
{
84+
$values = [];
85+
foreach ($parameters as $parameter) {
86+
$field = $this->casedField($parameter);
87+
$generated = (new FromDependency($this->notation))
88+
->then(new FromTypeDate($this->notation))
89+
->then(new FromCollection($this->notation))
90+
->then(new FromTypeBuiltin($this->notation))
91+
->then(new FromTypeAttributes($this->notation))
92+
->then(new FromEnum($this->notation))
93+
->then(new FromDefaultValue($this->notation))
94+
->then(new FromPreset($this->notation))
95+
->resolve($parameter, $presets);
96+
97+
if ($generated === null) {
98+
continue;
99+
}
100+
$values[$field] = $generated->content;
101+
}
102+
return Set::createFrom($values);
103+
}
104+
105+
private function locale(?string $locale): string
106+
{
107+
$fallback = static function (string $default = 'en_US'): string {
108+
$locale = stringify(getenv('FAKER_LOCALE'), $default);
109+
return empty($locale) ? $default : $locale;
110+
};
111+
return $locale ?? $fallback();
112+
}
113+
}

src/Testing/Faker/Resolver.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Constructo\Testing\Faker;
6+
7+
use Constructo\Support\Set;
8+
use Constructo\Support\Value;
9+
use ReflectionNamedType;
10+
use ReflectionParameter;
11+
use ReflectionType;
12+
use ReflectionUnionType;
13+
14+
use function count;
15+
16+
abstract class Resolver extends Faker
17+
{
18+
protected ?Resolver $previous = null;
19+
20+
final public function then(Resolver $resolver): Resolver
21+
{
22+
$resolver->previous($this);
23+
return $resolver;
24+
}
25+
26+
public function resolve(ReflectionParameter $parameter, Set $presets): ?Value
27+
{
28+
if (isset($this->previous)) {
29+
return $this->previous->resolve($parameter, $presets);
30+
}
31+
return null;
32+
}
33+
34+
final protected function previous(Resolver $previous): void
35+
{
36+
$this->previous = $previous;
37+
}
38+
39+
protected function detectReflectionType(?ReflectionType $type): ?string
40+
{
41+
if ($type instanceof ReflectionNamedType) {
42+
return $type->getName();
43+
}
44+
if ($type instanceof ReflectionUnionType) {
45+
$reflectionNamedTypes = $type->getTypes();
46+
$index = $this->generator->numberBetween(0, count($reflectionNamedTypes) - 1);
47+
return $this->detectReflectionType($reflectionNamedTypes[$index]);
48+
}
49+
return null;
50+
}
51+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Constructo\Testing\Faker\Resolver;
6+
7+
use Constructo\Support\Set;
8+
use Constructo\Support\Value;
9+
use Constructo\Type\Collection;
10+
use ReflectionClass;
11+
use ReflectionException;
12+
use ReflectionParameter;
13+
use Constructo\Testing\Faker\Resolver;
14+
15+
final class FromCollection extends Resolver
16+
{
17+
use MakeExtension;
18+
use ManagedExtension;
19+
20+
/**
21+
* @throws ReflectionException
22+
*/
23+
public function resolve(ReflectionParameter $parameter, Set $presets): ?Value
24+
{
25+
$collectionName = $this->detectCollectionName($parameter);
26+
if ($collectionName) {
27+
return $this->resolveCollection($collectionName);
28+
}
29+
return parent::resolve($parameter, $presets);
30+
}
31+
32+
/**
33+
* @param class-string<Collection> $collectionName
34+
* @throws ReflectionException
35+
*/
36+
private function resolveCollection(string $collectionName): Value
37+
{
38+
$reflection = new ReflectionClass($collectionName);
39+
$type = $this->detectCollectionType($reflection);
40+
41+
return new Value($type === null ? [] : $this->resolveCollectionFake($type));
42+
}
43+
44+
/**
45+
* @param class-string<object> $type
46+
* @throws ReflectionException
47+
*/
48+
private function resolveCollectionFake(string $type): array
49+
{
50+
$total = $this->generator->numberBetween(1, 5);
51+
return array_map(fn () => $this->fake($type)->toArray(), range(1, $total));
52+
}
53+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Constructo\Testing\Faker\Resolver;
6+
7+
use Constructo\Support\Set;
8+
use Constructo\Support\Value;
9+
use ReflectionException;
10+
use ReflectionParameter;
11+
use Constructo\Testing\Faker\Resolver;
12+
13+
final class FromDefaultValue extends Resolver
14+
{
15+
/**
16+
* @throws ReflectionException
17+
*/
18+
public function resolve(ReflectionParameter $parameter, Set $presets): ?Value
19+
{
20+
if ($parameter->isOptional() || $parameter->isDefaultValueAvailable()) {
21+
return new Value($parameter->getDefaultValue());
22+
}
23+
if ($parameter->allowsNull()) {
24+
return new Value(null);
25+
}
26+
return parent::resolve($parameter, $presets);
27+
}
28+
}

0 commit comments

Comments
 (0)