Skip to content

Commit

Permalink
feat: skip readonly properties on entities when generating factories (#…
Browse files Browse the repository at this point in the history
…798)

Co-authored-by: Nicolas PHILIPPE <[email protected]>
  • Loading branch information
KDederichs and nikophil authored Feb 1, 2025
1 parent 636cedd commit 9032c38
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 2 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ MONGO_URL="mongodb://127.0.0.1:27018/dbName?compressors=disabled&amp;gssapiServi
USE_DAMA_DOCTRINE_TEST_BUNDLE="0"
USE_FOUNDRY_PHPUNIT_EXTENSION="0"
PHPUNIT_VERSION="9" # allowed values: 9, 10, 11
APP_ENV=test
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"symfony/deprecation-contracts": "^2.2|^3.0",
"symfony/framework-bundle": "^6.4|^7.0",
"symfony/property-access": "^6.4|^7.0",
"symfony/property-info": "^6.4|^7.0",
"symfony/var-exporter": "^6.4.9|~7.0.9|^7.1.2",
"zenstruck/assert": "^1.4"
},
Expand Down
2 changes: 2 additions & 0 deletions src/Maker/Factory/FactoryGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public function __construct(
private \Traversable $defaultPropertiesGuessers,
private FactoryClassMap $factoryClassMap,
private NamespaceGuesser $namespaceGuesser,
private bool $forceProperties = false
) {
}

Expand Down Expand Up @@ -150,6 +151,7 @@ private function createMakeFactoryData(Generator $generator, string $class, Make
$this->staticAnalysisTool(),
$persisted ?? false,
$makeFactoryQuery->addPhpDoc(),
$this->forceProperties
);
}

Expand Down
25 changes: 25 additions & 0 deletions src/Maker/Factory/MakeFactoryData.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Doctrine\ORM\EntityRepository;
use Symfony\Bundle\MakerBundle\Str;
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Zenstruck\Foundry\ObjectFactory;
use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory;
use Zenstruck\Foundry\Persistence\Proxy;
Expand All @@ -29,6 +30,8 @@ final class MakeFactoryData
public const STATIC_ANALYSIS_TOOL_PHPSTAN = 'phpstan';
public const STATIC_ANALYSIS_TOOL_PSALM = 'psalm';

private static ReflectionExtractor|null $propertyInfo = null;

/** @var list<string> */
private array $uses;
/** @var array<string, string> */
Expand All @@ -43,6 +46,7 @@ public function __construct(
private string $staticAnalysisTool,
private bool $persisted,
bool $withPhpDoc,
private bool $forceProperties
) {
$this->uses = [
$this->getFactoryClass(),
Expand Down Expand Up @@ -154,6 +158,22 @@ public function addDefaultProperty(string $propertyName, string $defaultValue):
public function getDefaultProperties(): array
{
$defaultProperties = $this->defaultProperties;
$class = $this->object->getName();

/**
* If forceProperties is not set we filter out properties that can not be set because they're either readonly or have no setter.
* Useful for properties that auto generate when the entity is created and can not be changed like a createdAt property for example.
*
* We do this here because we need to get the class of the Entity which only seems to be accessible here.
*/
$defaultProperties = array_filter($defaultProperties, function (string $propertyName) use ($class): bool {
if (true === $this->forceProperties) {
return true;
}

return self::propertyInfo()->isWritable($class, $propertyName) || self::propertyInfo()->isInitializable($class, $propertyName);
}, ARRAY_FILTER_USE_KEY);

\ksort($defaultProperties);

return $defaultProperties;
Expand Down Expand Up @@ -189,4 +209,9 @@ public function addEnumDefaultProperty(string $propertyName, string $enumClass):
"self::faker()->randomElement({$enumShortClassName}::cases()),",
);
}

private static function propertyInfo(): ReflectionExtractor
{
return self::$propertyInfo ??= new ReflectionExtractor();
}
}
3 changes: 3 additions & 0 deletions src/ZenstruckFoundryBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,9 @@ public function loadExtension(array $config, ContainerConfigurator $configurator
if (!isset($bundles['DoctrineBundle']) && !isset($bundles['DoctrineMongoDBBundle'])) {
$container->removeDefinition('.zenstruck_foundry.maker.factory.doctrine_scalar_fields_default_properties_guesser');
}

$container->getDefinition('.zenstruck_foundry.maker.factory.generator')
->setArgument('$forceProperties', $config['instantiator']['always_force_properties'] ?? false);
} else {
$configurator->import('../config/command_stubs.php');
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

/*
* 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 App\Factory;

use Zenstruck\Foundry\ObjectFactory;
use Zenstruck\Foundry\Tests\Fixture\ObjectWithNonWriteable;

/**
* @extends ObjectFactory<ObjectWithNonWriteable>
*/
final class ObjectWithNonWriteableFactory extends ObjectFactory
{
/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services
*
* @todo inject services if required
*/
public function __construct()
{
}

public static function class(): string
{
return ObjectWithNonWriteable::class;
}

/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories
*
* @todo add your default values here
*/
protected function defaults(): array|callable
{
return [
'bar' => self::faker()->sentence(),
'baz' => self::faker()->sentence(),
'foo' => self::faker()->sentence(),
];
}

/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization
*/
protected function initialize(): static
{
return $this
// ->afterInstantiate(function(ObjectWithNonWriteable $objectWithNonWriteable): void {})
;
}
}
58 changes: 58 additions & 0 deletions tests/Fixture/Maker/expected/does_not_initialize_non_settable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

/*
* 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 App\Factory;

use Zenstruck\Foundry\ObjectFactory;
use Zenstruck\Foundry\Tests\Fixture\ObjectWithNonWriteable;

/**
* @extends ObjectFactory<ObjectWithNonWriteable>
*/
final class ObjectWithNonWriteableFactory extends ObjectFactory
{
/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services
*
* @todo inject services if required
*/
public function __construct()
{
}

public static function class(): string
{
return ObjectWithNonWriteable::class;
}

/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories
*
* @todo add your default values here
*/
protected function defaults(): array|callable
{
return [
'baz' => self::faker()->sentence(),
'foo' => self::faker()->sentence(),
];
}

/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization
*/
protected function initialize(): static
{
return $this
// ->afterInstantiate(function(ObjectWithNonWriteable $objectWithNonWriteable): void {})
;
}
}
41 changes: 41 additions & 0 deletions tests/Fixture/ObjectWithNonWriteable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

/*
* 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;

final class ObjectWithNonWriteable
{
private string $bar;
// @phpstan-ignore-next-line We do not want assign a default value to this so the factory sets one
private string $baz;

public function __construct(
public readonly string $foo
) {
$this->bar = 'bar';
}

public function getBaz(): string
{
return $this->baz;
}

public function setBaz(string $baz): ObjectWithNonWriteable
{
$this->baz = $baz;
return $this;
}

public function getBar(): string
{
return $this->bar;
}
}
4 changes: 4 additions & 0 deletions tests/Fixture/TestKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load
{
parent::configureContainer($c, $loader);

if ($this->getEnvironment() !== 'test') {
$loader->load(\sprintf('%s/config/%s.yaml', __DIR__, $this->getEnvironment()));
}

$c->loadFromExtension('zenstruck_foundry', [
'orm' => [
'reset' => [
Expand Down
3 changes: 3 additions & 0 deletions tests/Fixture/config/always_force.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
zenstruck_foundry:
instantiator:
always_force_properties: true # always "force set" properties
31 changes: 29 additions & 2 deletions tests/Integration/Maker/MakeFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Zenstruck\Foundry\Tests\Fixture\Entity\WithEmbeddableEntity;
use Zenstruck\Foundry\Tests\Fixture\Object1;
use Zenstruck\Foundry\Tests\Fixture\ObjectWithEnum;
use Zenstruck\Foundry\Tests\Fixture\ObjectWithNonWriteable;

/**
* @author Kevin Bond <[email protected]>
Expand Down Expand Up @@ -421,14 +422,40 @@ public function can_create_factory_with_default_enum(): void
$this->assertFileFromMakerSameAsExpectedFile(self::tempFile('src/Factory/ObjectWithEnumFactory.php'));
}

/**
* @test
*/
public function does_not_initialize_non_settable(): void
{
$tester = $this->makeFactoryCommandTester();

$tester->execute(['class' => ObjectWithNonWriteable::class, '--no-persistence' => true]);

$this->assertFileFromMakerSameAsExpectedFile(self::tempFile('src/Factory/ObjectWithNonWriteableFactory.php'));
}

/**
* @test
*/
public function does_force_initialization_of_non_settable_with_always_force(): void
{
$tester = $this->makeFactoryCommandTester('always_force');

$tester->execute(['class' => ObjectWithNonWriteable::class, '--no-persistence' => true]);

$this->assertFileFromMakerSameAsExpectedFile(self::tempFile('src/Factory/ObjectWithNonWriteableFactory.php'));
}

private function emulateSCAToolEnabled(string $scaToolFilePath): void
{
\mkdir(\dirname($scaToolFilePath), 0777, true);
\touch($scaToolFilePath);
}

private function makeFactoryCommandTester(): CommandTester
private function makeFactoryCommandTester(string $appEnv = 'test'): CommandTester
{
return new CommandTester((new Application(self::bootKernel()))->find('make:factory'));
return new CommandTester((new Application(self::bootKernel([
'environment' => $appEnv,
])))->find('make:factory'));
}
}

0 comments on commit 9032c38

Please sign in to comment.