Skip to content

Commit

Permalink
feat: validate objects
Browse files Browse the repository at this point in the history
  • Loading branch information
nikophil committed Feb 2, 2025
1 parent 34101a7 commit dc33524
Show file tree
Hide file tree
Showing 14 changed files with 271 additions and 9 deletions.
8 changes: 7 additions & 1 deletion bin/console
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,11 @@ use Zenstruck\Foundry\Tests\Fixture\TestKernel;

require_once __DIR__ . '/../tests/bootstrap.php';

$application = new Application(new TestKernel('test', true));
foreach ($argv ?? [] as $i => $arg) {
if (($arg === '--env' || $arg === '-e') && isset($argv[$i + 1])) {
$_ENV['APP_ENV'] = $argv[$i + 1];
break;
}
}
$application = new Application(new TestKernel($_ENV['APP_ENV'], true));
$application->run();
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
"doctrine/common": "^3.2.2",
"doctrine/doctrine-bundle": "^2.10",
"doctrine/doctrine-migrations-bundle": "^2.2|^3.0",
"doctrine/mongodb-odm-bundle": "^4.6|^5.0",
"doctrine/mongodb-odm": "^2.4",
"doctrine/mongodb-odm-bundle": "^4.6|^5.0",
"doctrine/orm": "^2.16|^3.0",
"phpunit/phpunit": "^9.5.0 || ^10.0 || ^11.0",
"symfony/console": "^6.4|^7.0",
Expand All @@ -46,6 +46,7 @@
"symfony/runtime": "^6.4|^7.0",
"symfony/translation-contracts": "^3.4",
"symfony/uid": "^6.4|^7.0",
"symfony/validator": "^6.4|^7.0",
"symfony/var-dumper": "^6.4|^7.0",
"symfony/yaml": "^6.4|^7.0"
},
Expand Down
8 changes: 8 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
use Faker;
use Zenstruck\Foundry\Configuration;
use Zenstruck\Foundry\FactoryRegistry;
use Zenstruck\Foundry\Object\Event\AfterInstantiate;
use Zenstruck\Foundry\Object\Instantiator;
use Zenstruck\Foundry\Object\ValidationListener;
use Zenstruck\Foundry\StoryRegistry;

return static function (ContainerConfigurator $container): void {
Expand Down Expand Up @@ -33,7 +35,13 @@
service('.zenstruck_foundry.story_registry'),
service('.zenstruck_foundry.persistence_manager')->nullOnInvalid(),
service('event_dispatcher'),
param('.zenstruck_foundry.validation_enabled'),
abstract_arg('validation_available'),
])
->public()

->set('.zenstruck_foundry.validation_listener', ValidationListener::class)
->args([service('validator')])
->tag('kernel.event_listener', ['event' => AfterInstantiate::class, 'method' => '__invoke'])
;
};
32 changes: 32 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1187,6 +1187,38 @@ You can even create associative arrays, with the nice DX provided by Foundry:
// will create ['prop1' => 'foo', 'prop2' => 'default value 2']
$array = SomeArrayFactory::createOne(['prop1' => 'foo']);

Validate your objects
~~~~~~~~~~~~~~~~~~~~~

Foundry can validate your objects automatically after they are instantiated. This can be useful to
ensure that your objects are in a valid state before they are used in your tests.

You can either enable validation them globally:

.. configuration-block::

.. code-block:: yaml
# config/packages/zenstruck_foundry.yaml
when@dev: # see Bundle Configuration section about sharing this in the test environment
zenstruck_foundry:
instantiator:
validate: true
Or enable/disable it for a specific factory, or in a specific test with methods ``withValidation()`` / ``withoutValidation()``:

::

class UserFactory extends ObjectFactory
{
protected function initialize(): static
{
return $this->withValidation();
// you can also disable validation using ->withoutValidation()
}
}


Using with DoctrineFixturesBundle
---------------------------------

Expand Down
2 changes: 2 additions & 0 deletions src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public function __construct(
public readonly StoryRegistry $stories,
private readonly ?PersistenceManager $persistence = null,
private readonly ?EventDispatcherInterface $eventDispatcher = null,
public readonly bool $validationEnabled = false,
public readonly bool $validationAvailable = false,
) {
$this->instantiator = $instantiator;
}
Expand Down
30 changes: 30 additions & 0 deletions src/Object/ValidationListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Zenstruck\Foundry\Object;

use Symfony\Component\Validator\Exception\ValidationFailedException;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Zenstruck\Foundry\Object\Event\AfterInstantiate;

final class ValidationListener
{
public function __construct(
private readonly ValidatorInterface $validator
) {
}

public function __invoke(AfterInstantiate $event): void
{
if (!$event->factory->validationEnabled()) {
return;
}

$violations = $this->validator->validate($event->object);

if ($violations->count() > 0) {
throw new ValidationFailedException($event->object, $violations);
}
}
}
46 changes: 46 additions & 0 deletions src/ObjectFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ abstract class ObjectFactory extends Factory
/** @phpstan-var InstantiatorCallable|null */
private $instantiator;

private bool $validationEnabled;

// keep an empty constructor for BC
public function __construct()
{
parent::__construct();

$this->validationEnabled = Configuration::instance()->validationEnabled;
}

/**
* @return class-string<T>
*/
Expand Down Expand Up @@ -105,6 +115,42 @@ public function afterInstantiate(callable $callback): static
return $clone;
}

/**
* @psalm-return static<T>
* @phpstan-return static
*/
public function withValidation(): static
{
if (!Configuration::instance()->validationAvailable) {
throw new \LogicException('Validation is not available. Make sure the "symfony/validator" package is installed and validation enabled.');
}

$clone = clone $this;
$clone->validationEnabled = true;

return $clone;
}

/**
* @psalm-return static<T>
* @phpstan-return static
*/
public function withoutValidation(): static
{
$clone = clone $this;
$clone->validationEnabled = false;

return $clone;
}

/**
* @internal
*/
public function validationEnabled(): bool
{
return $this->validationEnabled;
}

/**
* @internal
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Persistence/PersistentObjectFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ protected function normalizeObject(object $object): object
}
}

final protected function isPersisting(): bool
final public function isPersisting(): bool
{
$config = Configuration::instance();

Expand Down
39 changes: 33 additions & 6 deletions src/ZenstruckFoundryBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Zenstruck\Foundry\Mongo\MongoResetter;
use Zenstruck\Foundry\Object\Instantiator;
use Zenstruck\Foundry\ORM\ResetDatabase\MigrateDatabaseResetter;
Expand Down Expand Up @@ -87,6 +89,16 @@ public function configure(DefinitionConfigurator $definition): void
->example('my_instantiator')
->defaultNull()
->end()
->arrayNode('validation')
->info('Automatically validate the objects created.')
->canBeEnabled()
->validate()
->ifTrue(function(array $validation): bool {
return $validation['enabled'] && !interface_exists(ValidatorInterface::class);
})
->thenInvalid('Validation support cannot be enabled as the Validator component is not installed. Try running "composer require --dev symfony/validator".')
->end()
->end()
->end()
->end()
->arrayNode('global_state')
Expand Down Expand Up @@ -188,19 +200,16 @@ public function configure(DefinitionConfigurator $definition): void
->end()
->end()
->end()
->booleanNode('enable_validation')->defaultFalse()->end()
->end()
;
}

public function loadExtension(array $config, ContainerConfigurator $configurator, ContainerBuilder $container): void // @phpstan-ignore missingType.iterableValue
{
$container->registerForAutoconfiguration(Factory::class)
->addTag('foundry.factory')
;
$container->registerForAutoconfiguration(Factory::class)->addTag('foundry.factory');

$container->registerForAutoconfiguration(Story::class)
->addTag('foundry.story')
;
$container->registerForAutoconfiguration(Story::class)->addTag('foundry.story');

$configurator->import('../config/services.php');

Expand Down Expand Up @@ -282,6 +291,8 @@ public function loadExtension(array $config, ContainerConfigurator $configurator
->replaceArgument(0, $config['mongo']['reset']['document_managers'])
;
}

$container->setParameter('.zenstruck_foundry.validation_enabled', $config['instantiator']['validation']['enabled']);
}

public function build(ContainerBuilder $container): void
Expand All @@ -300,6 +311,22 @@ public function process(ContainerBuilder $container): void
->addMethodCall('addProvider', [new Reference($id)])
;
}

// validation
$container->getDefinition('.zenstruck_foundry.configuration')
->replaceArgument(7, $container->has('validator'));

if (!interface_exists(ValidatorInterface::class)) {
$container->removeDefinition('.zenstruck_foundry.validation_listener');
}

if ($container->has('.zenstruck_foundry.configuration') && !$container->has('validator')) {
if ($container->getParameter('.zenstruck_foundry.validation_enabled') === true) {
throw new LogicException('Validation support cannot be enabled because the validation is not enabled. Please, add enable validation with configuration "framework.validation: true".');
}

$container->removeDefinition('.zenstruck_foundry.validation_listener');
}
}

/**
Expand Down
29 changes: 29 additions & 0 deletions tests/Fixture/Entity/EntityForValidation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

/*
* This file is part of the zenstruck/foundry package.
*
* (c) Kevin Bond <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Zenstruck\Foundry\Tests\Fixture\Entity;

use Doctrine\ORM\Mapping as ORM;
use Zenstruck\Foundry\Tests\Fixture\Model\Base;
use Symfony\Component\Validator\Constraints as Assert;

#[ORM\Entity]
class EntityForValidation extends Base
{
public function __construct(
#[ORM\Column()]
#[Assert\NotBlank()]
public string $name = '',
) {
}
}
2 changes: 2 additions & 0 deletions tests/Fixture/config/validation_available.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
framework:
validation: true
6 changes: 6 additions & 0 deletions tests/Fixture/config/validation_enabled.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
framework:
validation: true

zenstruck_foundry:
instantiator:
validation: true
3 changes: 3 additions & 0 deletions tests/Fixture/config/validation_not_available.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
zenstruck_foundry:
instantiator:
validation: true
Loading

0 comments on commit dc33524

Please sign in to comment.