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
  • Loading branch information
KDederichs committed Jan 29, 2025
1 parent d8c8c46 commit 26233a4
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 15 deletions.
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
12 changes: 12 additions & 0 deletions src/Maker/Factory/FactoryGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ final class FactoryGenerator
public const PHPSTAN_PATH = '/vendor/phpstan/phpstan/phpstan';
public const PSALM_PATH = '/vendor/vimeo/psalm/psalm';

/** @var string[]|true */
private array|bool $forceProperties = [];

/** @param \Traversable<int, DefaultPropertiesGuesser> $defaultPropertiesGuessers */
public function __construct(
private ?PersistenceManager $persistenceManager,
Expand Down Expand Up @@ -150,6 +153,7 @@ private function createMakeFactoryData(Generator $generator, string $class, Make
$this->staticAnalysisTool(),
$persisted ?? false,
$makeFactoryQuery->addPhpDoc(),
$this->forceProperties
);
}

Expand All @@ -161,4 +165,12 @@ private function staticAnalysisTool(): string
default => MakeFactoryData::STATIC_ANALYSIS_TOOL_NONE,
};
}

public function alwaysForce(string ...$properties): self
{
$clone = clone $this;
$clone->forceProperties = $properties ?: true;

return $clone;
}
}
41 changes: 26 additions & 15 deletions src/Maker/Factory/MakeFactoryData.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
use Symfony\Bundle\MakerBundle\Str;
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface;
use Zenstruck\Foundry\ObjectFactory;
use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory;
use Zenstruck\Foundry\Persistence\Proxy;
Expand All @@ -31,13 +32,19 @@ final class MakeFactoryData
public const STATIC_ANALYSIS_TOOL_PHPSTAN = 'phpstan';
public const STATIC_ANALYSIS_TOOL_PSALM = 'psalm';

/**
* @var null|PropertyAccessExtractorInterface&PropertyInitializableExtractorInterface

Check failure on line 36 in src/Maker/Factory/MakeFactoryData.php

View workflow job for this annotation

GitHub Actions / Static Analysis

PHPDoc tag @var has invalid value (null|PropertyAccessExtractorInterface&PropertyInitializableExtractorInterface): Unexpected token "&", expected TOKEN_OTHER at offset 53 on line 2
*/
private static mixed $propertyInfo = null;

/** @var list<string> */
private array $uses;
/** @var array<string, string> */
private array $defaultProperties = [];
/** @var list<MakeFactoryPHPDocMethod> */
private array $methodsInPHPDoc;
private PropertyInfoExtractor $propertyInfo;
/** @var string[]|true */
private array|bool $forceProperties;

public function __construct(

Check failure on line 49 in src/Maker/Factory/MakeFactoryData.php

View workflow job for this annotation

GitHub Actions / Static Analysis

Method Zenstruck\Foundry\Maker\Factory\MakeFactoryData::__construct() has parameter $forceProperties with no value type specified in iterable type array.
private \ReflectionClass $object,
Expand All @@ -46,6 +53,7 @@ public function __construct(
private string $staticAnalysisTool,
private bool $persisted,
bool $withPhpDoc,
array|bool $forceProperties
) {
$this->uses = [
$this->getFactoryClass(),
Expand All @@ -65,14 +73,7 @@ public function __construct(
}

$this->methodsInPHPDoc = $withPhpDoc ? MakeFactoryPHPDocMethod::createAll($this) : [];
$reflectionExtractor = new ReflectionExtractor();
$this->propertyInfo = new PropertyInfoExtractor(
[],
[],
[],
[$reflectionExtractor],
[$reflectionExtractor]
);
$this->forceProperties = $forceProperties;

Check failure on line 76 in src/Maker/Factory/MakeFactoryData.php

View workflow job for this annotation

GitHub Actions / Static Analysis

Property Zenstruck\Foundry\Maker\Factory\MakeFactoryData::$forceProperties (array<string>|true) does not accept array|bool.
}

// @phpstan-ignore-next-line
Expand Down Expand Up @@ -167,12 +168,17 @@ public function getDefaultProperties(): array
$defaultProperties = $this->defaultProperties;
$clazz = $this->object->getName();

foreach ($defaultProperties as $propertyName => $propertyDefault) {
if ($this->propertyInfo->isWritable($clazz, $propertyName) || $this->propertyInfo->isInitializable($clazz, $propertyName)) {
continue;
$defaultProperties = array_filter($defaultProperties, function (string $propertyName) use ($clazz): bool {
if (true === $this->forceProperties) {
return true;
}
unset($defaultProperties[$propertyName]);
}

if (is_array($this->forceProperties) && \in_array($propertyName, $this->forceProperties, true)) {
return true;
}

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

\ksort($defaultProperties);

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

private static function propertyInfo(): PropertyAccessExtractorInterface&PropertyInitializableExtractorInterface
{
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 @@ -341,6 +341,9 @@ private function configureInstantiator(array $config, ContainerBuilder $containe
$container->getDefinition('.zenstruck_foundry.instantiator')
->addMethodCall('alwaysForce', returnsClone: true)
;
$container->getDefinition('.zenstruck_foundry.maker.factory.generator')
->addMethodCall('alwaysForce', returnsClone: true)
;
}
}

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 [
'baz' => self::faker()->sentence(),
'bar' => 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 {})
;
}
}
12 changes: 12 additions & 0 deletions tests/Integration/Maker/MakeFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,18 @@ public function does_not_initialize_non_settable(): void
$this->assertFileFromMakerSameAsExpectedFile(self::tempFile('src/Factory/ObjectWithNonWriteableFactory.php'));
}

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

$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);
Expand Down

0 comments on commit 26233a4

Please sign in to comment.