Skip to content

Commit 9cca954

Browse files
Nyholmlocalheinz
andauthored
Adding support for extensions and PSR-11 (#154)
* Adding support for extensions and PSR-11 * Added getDefaultExtensions * minor * Apply suggestions from code review Co-authored-by: Andreas Möller <[email protected]> * CS fixes * Refactor container * CS * Renamed namespace * Updated names and namespaces * Fix: Run 'make cs' * Fix: Remove unnecessary initialization * Fix: Add missing throws annotation * Fix: Run 'make baseline' * Fix: Wrapping * Fix: Throw InvalidArgumentException * Fix: Run 'make cs' * Fix: Use elvis operator * Fix: Move exception to Extension namespace * Fix: Add return type declarations * Fix: Avoid unnecessary imports * Fix: Whitespace * Fix: Add covers annotation * Enhancement: Throw RuntimeException when service can not be resolved to extension * Fix: DocBlock * Fix: Run 'make cs' * Fix: Namespace * Enhancement: Merge Container into Extension namespace * Added dock block * cs Co-authored-by: Andreas Möller <[email protected]>
1 parent c5ed3d4 commit 9cca954

16 files changed

+1228
-1
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
}
1515
],
1616
"require": {
17-
"php": "^7.1 || ^8.0"
17+
"php": "^7.1 || ^8.0",
18+
"psr/container": "^1.0"
1819
},
1920
"require-dev": {
2021
"ext-intl": "*",

phpstan-baseline.neon

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ parameters:
1515
count: 1
1616
path: src/Faker/Calculator/Luhn.php
1717

18+
-
19+
message: "#^Result of && is always false\\.$#"
20+
count: 1
21+
path: src/Faker/Extension/ContainerBuilder.php
22+
23+
-
24+
message: "#^PHPDoc tag @throws with type Faker\\\\Extension\\\\ExtensionNotFound\\|Psr\\\\Container\\\\ContainerExceptionInterface is not subtype of Throwable$#"
25+
count: 1
26+
path: src/Faker/Generator.php
27+
1828
-
1929
message: "#^Unreachable statement \\- code above always terminates\\.$#"
2030
count: 1

psalm.baseline.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
<code>string</code>
99
</InvalidReturnType>
1010
</file>
11+
<file src="src/Faker/Generator.php">
12+
<InvalidThrow occurrences="1">
13+
<code>ContainerExceptionInterface</code>
14+
</InvalidThrow>
15+
</file>
1116
<file src="src/Faker/ORM/CakePHP/EntityPopulator.php">
1217
<UndefinedClass occurrences="1">
1318
<code>TableRegistry</code>

src/Faker/Core/File.php

Lines changed: 577 additions & 0 deletions
Large diffs are not rendered by default.

src/Faker/Extension/Container.php

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Faker\Extension;
6+
7+
use Psr\Container\ContainerInterface;
8+
9+
/**
10+
* A simple implementation of a container.
11+
*
12+
* @experimental This class is experimental and does not fall under our BC promise
13+
*/
14+
final class Container implements ContainerInterface
15+
{
16+
/**
17+
* @var array<string, callable|object|string>
18+
*/
19+
private $definitions;
20+
21+
private $services = [];
22+
23+
/**
24+
* Create a container object with a set of definitions. The array value MUST
25+
* produce an object that implements Extension.
26+
*
27+
* @param array<string, callable|object|string> $definitions
28+
*/
29+
public function __construct(array $definitions)
30+
{
31+
$this->definitions = $definitions;
32+
}
33+
34+
/**
35+
* @throws NotInContainerException
36+
* @throws ContainerException
37+
* @throws \InvalidArgumentException
38+
* @throws \RuntimeException
39+
*
40+
* @return Extension
41+
*/
42+
public function get($id)
43+
{
44+
if (!is_string($id)) {
45+
throw new \InvalidArgumentException(sprintf(
46+
'First argument of %s::get() must be string',
47+
self::class
48+
));
49+
}
50+
51+
if (array_key_exists($id, $this->services)) {
52+
return $this->services[$id];
53+
}
54+
55+
if (!$this->has($id)) {
56+
throw new NotInContainerException(sprintf(
57+
'There is not service with id "%s" in the container.',
58+
$id
59+
));
60+
}
61+
62+
$definition = $this->definitions[$id];
63+
64+
if (is_callable($definition)) {
65+
try {
66+
$service = $definition();
67+
} catch (\Throwable $e) {
68+
throw new ContainerException(
69+
sprintf(
70+
'Error while invoking callable for "%s"',
71+
$id
72+
),
73+
0,
74+
$e
75+
);
76+
}
77+
} elseif (is_object($definition)) {
78+
$service = $definition;
79+
} elseif (is_string($definition)) {
80+
if (!class_exists($definition)) {
81+
throw new ContainerException(sprintf(
82+
'Could not instantiate class "%s". Class was not found.',
83+
$id
84+
));
85+
}
86+
87+
try {
88+
$service = new $definition();
89+
} catch (\Throwable $e) {
90+
throw new ContainerException(
91+
sprintf(
92+
'Could not instantiate class "%s"',
93+
$id
94+
),
95+
0,
96+
$e
97+
);
98+
}
99+
} else {
100+
throw new ContainerException(sprintf(
101+
'Invalid type for definition with id "%s"',
102+
$id
103+
));
104+
}
105+
106+
if (!$service instanceof Extension) {
107+
throw new \RuntimeException(sprintf(
108+
'Service resolved for identifier "%s" does not implement the %s" interface.',
109+
$id,
110+
Extension::class
111+
));
112+
}
113+
114+
$this->services[$id] = $service;
115+
116+
return $service;
117+
}
118+
119+
/**
120+
* @throws \InvalidArgumentException
121+
*/
122+
public function has($id)
123+
{
124+
if (!is_string($id)) {
125+
throw new \InvalidArgumentException(sprintf(
126+
'First argument of %s::get() must be string',
127+
self::class
128+
));
129+
}
130+
131+
return array_key_exists($id, $this->definitions);
132+
}
133+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Faker\Extension;
6+
7+
use Faker\Core;
8+
use Psr\Container\ContainerInterface;
9+
10+
/**
11+
* @experimental This class is experimental and does not fall under our BC promise
12+
*/
13+
final class ContainerBuilder
14+
{
15+
/**
16+
* @var array<string, callable|object|string>
17+
*/
18+
private $definitions = [];
19+
20+
/**
21+
* @param callable|object|string $value
22+
*
23+
* @throws \InvalidArgumentException
24+
*/
25+
public function add($value, string $name = null): self
26+
{
27+
if (!is_string($value) && !is_callable($value) && !is_object($value)) {
28+
throw new \InvalidArgumentException(sprintf(
29+
'First argument to "%s::add()" must be a string, callable or object.',
30+
self::class
31+
));
32+
}
33+
34+
if ($name === null) {
35+
if (is_string($value)) {
36+
$name = $value;
37+
} elseif (is_object($value)) {
38+
$name = get_class($value);
39+
} else {
40+
throw new \InvalidArgumentException(sprintf(
41+
'Second argument to "%s::add()" is required not passing a string or object as first argument',
42+
self::class
43+
));
44+
}
45+
}
46+
47+
$this->definitions[$name] = $value;
48+
49+
return $this;
50+
}
51+
52+
public function build(): ContainerInterface
53+
{
54+
return new Container($this->definitions);
55+
}
56+
57+
/**
58+
* Get an array with extension that represent the default English
59+
* functionality.
60+
*/
61+
public static function defaultExtensions(): array
62+
{
63+
return [
64+
FileExtension::class => Core\File::class,
65+
];
66+
}
67+
68+
public static function getDefault(): ContainerInterface
69+
{
70+
$instance = new self();
71+
72+
foreach (self::defaultExtensions() as $id => $definition) {
73+
$instance->add($definition, $id);
74+
}
75+
76+
return $instance->build();
77+
}
78+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Faker\Extension;
6+
7+
use Psr\Container\ContainerExceptionInterface;
8+
9+
/**
10+
* @experimental This class is experimental and does not fall under our BC promise
11+
*/
12+
final class ContainerException extends \RuntimeException implements ContainerExceptionInterface
13+
{
14+
}

src/Faker/Extension/Extension.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Faker\Extension;
6+
7+
/**
8+
* An extension is the only way to add new functionality to Faker.
9+
*
10+
* @experimental This interface is experimental and does not fall under our BC promise
11+
*/
12+
interface Extension
13+
{
14+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Faker\Extension;
6+
7+
/**
8+
* @experimental This class is experimental and does not fall under our BC promise
9+
*/
10+
final class ExtensionNotFound extends \LogicException
11+
{
12+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace Faker\Extension;
4+
5+
/**
6+
* @experimental This interface is experimental and does not fall under our BC promise
7+
*/
8+
interface FileExtension extends Extension
9+
{
10+
/**
11+
* Get a random MIME type
12+
*
13+
* @example 'video/avi'
14+
*/
15+
public function mimeType(): string;
16+
17+
/**
18+
* Get a random file extension (without a dot)
19+
*
20+
* @example avi
21+
*/
22+
public function extension(): string;
23+
24+
/**
25+
* Get a full path to a new real file on the system.
26+
*/
27+
public function filePath(): string;
28+
}

0 commit comments

Comments
 (0)