Skip to content

Commit

Permalink
A FakerProvider for code bases with a PSR-11 compliant DI container (#1)
Browse files Browse the repository at this point in the history
* FakerProvider that pulls the Generator from a PSR-11 container.

As it stands, this code only supports pulling the container itself from a PHP file. That file may
either return the container object directly, or set it into a defined variable name. The provider
will include the PHP file defined to get the container, then return the Generator from the container
by using the defined ID.

By using the Generator from the DI container we ensure that all custom providers are added to the
Generator first and so making them available for analysis.

As an example... if using Symfony, something similar to:
    $provider = new PsrContainerFakerProvider(
        './var/cache/dev/App_KernelDevDebugContainer.php',
        null,
        'Faker\Generator'
    );

will give us a provider that, when asked for the Faker Generator, will include the
App_KernelDevDebugContainer.php file, using the return value as the container. From that container
the provider will then pull a service with the ID 'Faker\Generator' and return it.

* A factory that can provide a PsrContainerFakerProvider.

To allow for the service that provides \CalebDW\Fakerstan\FakerProvider to call the
static 'create' method on the defined factory, this factory takes parameters for its
constructor and then copies those values onto static member variables. The static
'create' method then has access to the values that were given to the constructor.

* Add information about PsrContainerFakerProviderFactory to documentation.

* Linting.

* Import classes instead of using FQCNs.

* Call TestCase methods on $this, instead of self.

* Use it* instead of getFaker*.

* Remove unnecessary comments.

* Just require the container file, instead of using require_once.

This removes the issue of the changing return value when calling require_once multiple
times. From that, it makes our tests easier to set up and tear down, too.

* Use the arguments key in example definition.

* If there is a problem loading the configured container file, include the configured path in the error message.

* Configuration settings for the PSRContainerFakerProvider.

Default values allow users that are not using the provider to not have to set values. These defaults
either match the default values that are already used in constructors or, in the case of the
$phpContainerPath, have a default value that will at least be useful when it is not overridden by
the user.

* Updated documentation.

* Remove the static variables, now that I understand the configuration file.

* Predefine the PsrContainerFakerProviderFactory service with its constructor parameters.

Now that we have default values for all the constructor parameters, we can define the
factory service ourselves. This stops the user from having to say "I'm using this
factory with these parameters" and then having to also say "now give me that service
with those parameters".

* Change namespace of parameters to fakerstan > psrProvider.

* Allow the container path to be null until we absolutely decide that we want to create a PsrContainerFakerProvider.

* Update src/PsrContainerFakerProviderFactory.php

Co-authored-by: Caleb White <[email protected]>

---------

Co-authored-by: Caleb White <[email protected]>
  • Loading branch information
ljmaskey and calebdw authored Dec 31, 2024
1 parent ed52fa3 commit 18f6c97
Show file tree
Hide file tree
Showing 9 changed files with 300 additions and 2 deletions.
40 changes: 38 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,44 @@ Faker instance used in the project (or at least an instance with the custom
providers added to it).

If using Laravel, FakerStan will automatically resolve the faker instance from
the container using the `fake()` helper function. If not using Laravel, you can
specify a custom factory class in the `phpstan.neon(.dist)` configuration file:
the container using the `fake()` helper function.

If not using Laravel, but you are using a framework or environment that has a PSR-11
compliant DI container (and assuming this container is configured via a PHP file), you
can use the PsrContainerFakerProviderFactory.

```neon
parameters:
fakerstan:
fakerProviderFactory: CalebDW\Fakerstan\PsrContainerFakerProviderFactory
psrProvider:
phpContainerPath: /path/to/container.php
...
```

The parameters that can be set are:
* `phpContainerPath`: the path to the PHP file that configures the container.
* `setsVariable`: if the file that configures the container assigns it to a global variable,
this parameter is used to indicate the name of that variable. If the container file returns
the container itself, set this parameter is null (which is the default).
* `containerFakerId`: the ID that the container uses for retrieving the Faker Generator. By
default, this parameter is the Generator's class name (ie. `Faker\Generator`).

If using Symfony, for example, you could use something like:

```neon
parameters:
fakerstan:
fakerProviderFactory: CalebDW\Fakerstan\PsrContainerFakerProviderFactory
psrProvider:
phpContainerPath: /opt/project/var/cache/dev/App_KernelDevDebugContainer.php
```

to use the PsrContainerFakerProviderFactory and set the path to the Symfony container. The default
values for the additional parameters are correct for Symfony.

If not using Laravel or some other PSR-11 compliant container, you can specify a custom
factory class in the `phpstan.neon(.dist)` configuration file:

```neon
parameters:
Expand Down
14 changes: 14 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
parameters:
fakerstan:
fakerProviderFactory: CalebDW\Fakerstan\FakerProviderFactory
psrProvider:
phpContainerPath: null
setsVariable: null
containerFakerId: Faker\Generator
parametersSchema:
fakerstan: structure([
fakerProviderFactory: string(),
psrProvider: structure([
phpContainerPath: schema(string(), nullable()),
setsVariable: schema(string(), nullable()),
containerFakerId: string()
])
])
services:
- class: CalebDW\Fakerstan\FakerProviderFactory
- class: CalebDW\Fakerstan\PsrContainerFakerProviderFactory
arguments:
phpContainerPath: %fakerstan.psrProvider.phpContainerPath%
setsVariable: %fakerstan.psrProvider.setsVariable%
containerFakerId: %fakerstan.psrProvider.containerFakerId%
- class: CalebDW\Fakerstan\FakerProvider
factory: @%fakerstan.fakerProviderFactory%::create
- class: CalebDW\Fakerstan\ProviderExtension
Expand Down
67 changes: 67 additions & 0 deletions src/PsrContainerFakerProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace CalebDW\Fakerstan;

use Faker\Generator;
use Psr\Container\ContainerInterface;
use RuntimeException;

final class PsrContainerFakerProvider implements FakerProvider
{
private ?Generator $generatorFromContainer = null;

public function __construct(
private string $phpContainerPath,
private ?string $setsVariable,
private string $containerFakerId,
) {
}

public function getFaker(): Generator
{
if (is_null($this->generatorFromContainer)) {
$this->generatorFromContainer = $this->getGeneratorFromContainer();
}

return $this->generatorFromContainer;
}

private function getGeneratorFromContainer(): Generator
{
if (! is_readable($this->phpContainerPath)) {
throw new RuntimeException('Could not read container PHP file ('.$this->phpContainerPath.')');
}

$maybeContainer = require $this->phpContainerPath;

if (is_null($this->setsVariable) && ($maybeContainer === 1)) {
throw new RuntimeException('Container file was expected to return the container, but it returned nothing');
}

if (is_string($this->setsVariable)) {
$definedVariables = get_defined_vars();
if (! array_key_exists($this->setsVariable, $definedVariables)) {
throw new RuntimeException('Container file does not set variable '.$this->setsVariable);
}

$maybeContainer = $definedVariables[$this->setsVariable];
}

if (! $maybeContainer instanceof ContainerInterface) {
throw new RuntimeException('Retrieved container is not a '.ContainerInterface::class);
}

if (! $maybeContainer->has($this->containerFakerId)) {
throw new RuntimeException('Container does not have entry with ID '.$this->containerFakerId);
}

$containerFaker = $maybeContainer->get($this->containerFakerId);
if (! $containerFaker instanceof Generator) {
throw new RuntimeException('Container entry with ID '.$this->containerFakerId.' is not a '.Generator::class);
}

return $containerFaker;
}
}
31 changes: 31 additions & 0 deletions src/PsrContainerFakerProviderFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace CalebDW\Fakerstan;

use Faker\Generator;
use RuntimeException;

class PsrContainerFakerProviderFactory
{
public function __construct(
private ?string $phpContainerPath = null,
private ?string $setsVariable = null,
private string $containerFakerId = Generator::class,
) {
}

public function create(): FakerProvider
{
if (is_null($this->phpContainerPath)) {
throw new RuntimeException(self::class.' requires a value for the "fakerstan.psrProvider.phpContainerPath" parameter');
}

return new PsrContainerFakerProvider(
$this->phpContainerPath,
$this->setsVariable,
$this->containerFakerId,
);
}
}
32 changes: 32 additions & 0 deletions tests/Fixtures/PsrContainerFakerProviderTest/ContainerClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace CalebDW\Fakerstan\Tests\Fixtures\PsrContainerFakerProviderTest;

use Exception;
use Faker\Generator;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use stdClass;

class ContainerClass implements ContainerInterface
{
public function get(string $id): mixed
{
return match ($id) {
'generatorId' => new Generator(),
'notGeneratorId' => new stdClass(),
default => throw new class() extends Exception implements NotFoundExceptionInterface {
},
};
}

public function has(string $id): bool
{
return match ($id) {
'generatorId', 'notGeneratorId' => true,
default => false,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

declare(strict_types=1);

namespace CalebDW\Fakerstan\Tests\Fixtures\PsrContainerFakerProviderTest;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

declare(strict_types=1);

namespace CalebDW\Fakerstan\Tests\Fixtures\PsrContainerFakerProviderTest;

return new ContainerClass();
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace CalebDW\Fakerstan\Tests\Fixtures\PsrContainerFakerProviderTest;

use stdClass;

$containerVariable = new ContainerClass();
$nonContainerVariable = new stdClass();
96 changes: 96 additions & 0 deletions tests/PsrContainerFakerProviderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

declare(strict_types=1);

namespace CalebDW\Fakerstan\Tests;

use CalebDW\Fakerstan\PsrContainerFakerProvider;
use Faker\Generator;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use RuntimeException;

#[CoversClass(PsrContainerFakerProvider::class)]
class PsrContainerFakerProviderTest extends TestCase
{
#[Test]
public function itUsesContainerReturnedFromFile()
{
$containerFilename = __DIR__.'/Fixtures/PsrContainerFakerProviderTest/ReturningContainerFile.php';
$sut = new PsrContainerFakerProvider($containerFilename, null, 'generatorId');
$faker = $sut->getFaker();

$this->assertInstanceOf(Generator::class, $faker);
}

#[Test]
public function itUsesContainerSetInVariable()
{
$containerFilename = __DIR__.'/Fixtures/PsrContainerFakerProviderTest/VariableSettingContainerFile.php';
$sut = new PsrContainerFakerProvider($containerFilename, 'containerVariable', 'generatorId');
$faker = $sut->getFaker();

$this->assertInstanceOf(Generator::class, $faker);
}

#[Test]
public function itThrowsExceptionWhenUnableToReadContainerFile()
{
$this->expectException(RuntimeException::class);

$containerFilename = __DIR__.'/a-file-that-does-not-exist';
$sut = new PsrContainerFakerProvider($containerFilename, null, 'generatorId');
$sut->getFaker();
}

#[Test]
public function itThrowsExceptionWhenTheContainerFileIsExpectedToReturnContainerButDoesNot()
{
$this->expectException(RuntimeException::class);

$containerFilename = __DIR__.'/Fixtures/PsrContainerFakerProviderTest/EmptyContainerFile.php';
$sut = new PsrContainerFakerProvider($containerFilename, null, 'generatorId');
$sut->getFaker();
}

#[Test]
public function itThrowsExceptionWhenTheContainerFileIsExpectedToSetAVariableButDoesNot()
{
$this->expectException(RuntimeException::class);

$containerFilename = __DIR__.'/Fixtures/PsrContainerFakerProviderTest/EmptyContainerFile.php';
$sut = new PsrContainerFakerProvider($containerFilename, 'containerVariable', 'generatorId');
$sut->getFaker();
}

#[Test]
public function itThrowsExceptionWhenTheDeterminedContainerIsNotActuallyAContainer()
{
$this->expectException(RuntimeException::class);

$containerFilename = __DIR__.'/Fixtures/PsrContainerFakerProviderTest/VariableSettingContainerFile.php';
$sut = new PsrContainerFakerProvider($containerFilename, 'nonContainerVariable', 'generatorId');
$sut->getFaker();
}

#[Test]
public function itThrowsExceptionWhenTheContainerDoesNotHaveServiceWithId()
{
$this->expectException(RuntimeException::class);

$containerFilename = __DIR__.'/Fixtures/PsrContainerFakerProviderTest/ReturningContainerFile.php';
$sut = new PsrContainerFakerProvider($containerFilename, null, 'id-not-in-container');
$sut->getFaker();
}

#[Test]
public function itThrowsExceptionWhenTheNamedServiceIsNotAGenerator()
{
$this->expectException(RuntimeException::class);

$containerFilename = __DIR__.'/Fixtures/PsrContainerFakerProviderTest/ReturningContainerFile.php';
$sut = new PsrContainerFakerProvider($containerFilename, null, 'notGeneratorId');
$sut->getFaker();
}
}

0 comments on commit 18f6c97

Please sign in to comment.