-
-
Notifications
You must be signed in to change notification settings - Fork 75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to make sure Entity association is valid from both sides? #531
Comments
I already thought about using the
I think I'd need to hook in after the Category is created, but before its attributes get normalized. As I think the attributes get normalized before the category gets created, maybe I need to prevent the Sadly, setting them as |
Hello @janopae indeed, unfortunately there is currently no easy solution to do it. However, I'd thought you'de be able to do it with have you tried to use another name than |
I recently had the case and resolved it like this: class Program{}
class ProgramTranslation
{
public function __construct(private Program $program)
{
$program->addProgramTranslation($program);
}
}
// ProgramFactory
public function withTranslations(Language ...$languages): static
{
return $this->addState([
'programTranslationsFactory' => static fn (Program $program) => ProgramTranslationFactory::new()->sequence(
\array_map(
static fn (Language $language) => ['language' => $language, 'program' => $program],
$languages
)
)->create(),
]);
}
public function initialize(): self
{
return $this->instantiateWith(
(new Instantiator())
->alwaysForceProperties()
->allowExtraAttributes(['programTranslationsFactory'])
)->afterInstantiate(
static function (Program $p, array $attributes) {
if (!isset($attributes['programTranslationsFactory'])) {
return;
}
($attributes['programTranslationsFactory'])($p);
}
);
} |
This should be added to the docs I guess 🙏 |
Hi Oskar, I think you're right. However, I think it will be fixed in foundry 2.0 |
@nikophil Thanks for the solution/workaround! Yes, that works – by wrapping it in a callback, I can successfully keep the instance from being automatically created before I generalised it a bit, so it handles every attempt to set the return $this->addState(['translations' => ProgramTranslationFactory::new()->sequence(
\array_map(
static fn (Language $language) => ['language' => $language, 'program' => $program],
$languages
)
)
]); And from the outside, you can assign any This version of the code looks something like this: class ProgramFactory
{
protected function initialize(): self
{
return $this
->instantiateWith((new Instantiator())->allowExtraAttributes(['translations']))
->beforeInstantiate(function (array $attributes) {
if (!isset($attributes['translations'])) {
return $attributes;
}
$translationFactories = $attributes['translations'];
$attributes['translations'] = function (Program $program) use ($translationFactories): void {
if ($translationFactories instanceof FactoryCollection) {
$translationFactories = $translationFactories->all();
}
foreach ($translationFactories as $translationFactory) {
if (!$translationFactory instanceof TranslationFactory) {
throw new \InvalidArgumentException('Got ' . get_debug_type($translationFactory));
}
$translationFactory->create(['program' => $program]);
}
};
return $attributes;
})
->afterInstantiate(function (Program $program, array $attributes): void {
if (!isset($attributes['translations'])) {
return;
}
$attributes['translations']($program);
});
}
} Edit: I generalised the implementation even further: /**
* @see https://github.com/zenstruck/foundry/issues/531
*
* Usage:
*
* In the `initialize()` function of your factory:
*
* ```
* return $this
* ->instantiateWith((new Instantiator())->allowExtraAttributes(['children']))
* ->beforeInstantiate(ChildRelationHelper::prepareCreationOfChildEntitiesWithAReferenceToParent('children', 'parent'))
* ->afterInstantiate(ChildRelationHelper::createChildEntitiesWithAReferenceToParent('children'))
* ;
*```
*
*/
class ChildRelationHelper
{
/**
* Prevents objects of a child relation to be created without a reference to their parent in a factory, and prepares
* the creation using {@see self::createChildEntitiesWithAReferenceToParent() } if passed to {@see ModelFactory::$beforeInstantiate}.
*
* Requires the instantiator passed to {@see ModelFactory::instantiateWith()} to have {@see Instantiator::allowExtraAttributes()}
* set with $childRelationNameOnParent.
*/
final public static function prepareCreationOfChildEntitiesWithAReferenceToParent(string $childRelationNameOnParent, string $parentRelationNameOnChild): callable
{
return static function (array $attributes) use ($childRelationNameOnParent, $parentRelationNameOnChild) {
if (!isset($attributes[$childRelationNameOnParent])) {
return $attributes;
}
$childFactories = $attributes[$childRelationNameOnParent];
$attributes[$childRelationNameOnParent] = static function ($parent) use ($parentRelationNameOnChild, $childFactories): void {
if ($childFactories instanceof FactoryCollection) {
$childFactories = $childFactories->all();
}
foreach ($childFactories as $childFactory) {
if (!$childFactory instanceof ModelFactory) {
throw new \InvalidArgumentException('Got '.get_debug_type($childFactory));
}
$childFactory->create([$parentRelationNameOnChild => $parent]);
}
};
return $attributes;
};
}
/**
* Creates instances of a child relation with a reference to its parent if provided to { @see ModelFactory::afterInstantiate() }.
* Requires creation to be prepared using {@see self::prepareCreationOfChildEntitiesWithAReferenceToParent() }.
*/
final public static function createChildEntitiesWithAReferenceToParent(string $childRelationNameOnParent): callable
{
return function ($parent, array $attributes) use ($childRelationNameOnParent): void {
if (!isset($attributes[$childRelationNameOnParent])) {
return;
}
$attributes[$childRelationNameOnParent]($parent);
};
}
} |
Because I'm curious: How will this use case be addressed in foundry 2.0? |
I think the creation of children is delegated to a post persist callback. |
hey @janopae it appears it is even easier to directly use a "after instantiate" callback without even adding an extra parameter: // ProgramFactory
public function withTranslation(array $translationAttributes): static
{
return $this->afterInstantiate(
static function (Program $program) use ($translationAttributes) {
ProgramTranslationFactory::new([
'program' => $program,
...$translationAttributes
])
->create();
}
);
}
// in some test
ProgramFactory::new()
->withTranslation([/** some attributes for translation 1 */])
->withTranslation([/** some attributes for translation 2 */])
->create() I like how it fits well with the fluent interfaces. you can even test if |
If you have a bidirectional OneToMany/ManyToOne assoziation between two entities, you need to make sure it gets updated on both sides. Your code might look like this:
If you want to create an instance, just do
And both sides of the association will be up to date.
Now if you want to create a category in Foundry, you'd probably like to do something like this:
How can you write the Factories in such a way that they pass the right Category into the Post's constructor?
The text was updated successfully, but these errors were encountered: