You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
with a DDD approach, all repositories would be interfaces, which opens the door to have multiple implementations: usually a Doctrine one and a "in-memory" one.
In one of my project, we use two different kernels in test: the common one, and a InMemoryKernel one, which injects in-memory repositories instead of doctrine ones.
Most of the factories have the following method, which will store the new object in a property of the in-memory repository:
Beside of this, I'm testing all my "doctrine" and "in-memory" repositories with the exact same class, using an abstract test class and two implementations which only have a setUp() method. This way I can be sure both implementations behave the same way and I know I can safely replace my doctrine repositories by the in-memory ones. Resulting in super fast kernel tests!
Another very cool benefit is that we do not have to mock repositories anymore! Once the in-memory factories are initialized, we just need to call $factory->create() and the object is directly available in the tested code.
My current implementation works well, but is very perfectible, mainly because in some cases it is a little bit hard to work with relationships. I really think this is a very nice feature, which should be in Foundry!
About the implementation I have in mind:
First, I'd like this behavior to be globally enabled or disabled with some kind of marker. Not sure which kind of marker: maybe in a first implementation, a simple function like enable_in_memory() will suffice. But IMO the best way would be an attribute on the test method or test class.
We must introduce an interface for the in memory repositories:
/** * @template T ofobject */interface InMemoryRepository
{
/** * @param T $object */publicfunction_save(object$object): void;
// the underscore prefix is needed in order to notconflict// with a potential `save()` method in `SomeObjectRepositoryInterface`
}
Then, when the option is enabled, we need a way to create in-memory factories. We'd need to hook in the factory creation process, in order to expose only in-memory factories, when the feature is enabled.
This will need a little bit of refactoring because currently, when a factory is not a service, we create it with a static call: Factory::new()
My suggestion would be to change how FactoryRegistry behaves and to create the factory inside of it, if we don't find it in the container.
// Zenstruck\Foundry\FactoryRegistry
- public function get(string $class): ?Factory+ public function get(string $class): Factory
{
foreach ($this->factories as $factory) {
if ($class === $factory::class) {
return $factory;
}
}
- return null;+ return new $class(); // todo: handle `ArgumentCountError`
}
(it would become a.... FactoryFactory 😱 but this name is really awful, I think we can keep the current name)
Then, we could extract an interface from the FactoryRegistry and decorate it with InMemoryFactoryRegistry:
// Zenstruck\Foundry\InMemory\InMemoryFactoryRegistrypublicfunctionget(string$class): Factory
{
if (/** in memory is not enabled */) {
return$this->decorated->get($class);
}
return$this->decorated
->get($class)
->withoutPersisting()
->afterInstantiate(
staticfn (object$object) => $this->findInMemoryRepository($class)->_save($object)
)
;
}
We also need a way to guess the in-memory repository from the factory's class name. One of the solutions would be to introduce an new attribute #[AsInMemoryRepository(class: Object::class)].
And voilà! 🎉 this is all we need as a first step.
Next step would be to to provide a in-memory version of RepositoryDecorator because, I'd really like to be able to use things like ::findOrCreate() within the in-memory tests. And then RepositoryAssertions should also be needed! But let's keep simple for a first iteration 😅
As a bonus, I think we can isolate all the code into a Zenstruck\Foundry\InMemory namespace, which will eventually have its own repo in the future.
Do you have any thoughts about this?
The text was updated successfully, but these errors were encountered:
nikophil
changed the title
[Foundry 2] support "in memory" repositories
support "in memory" repositories
Dec 1, 2023
Sure, this all makes sense to me. Once we create the 2.x branch and all the legacy stuff from 1.x is removed, let's experiment! We can at least ensure it will be possible to add to 2.x w/o a BC break if the feature is not quite ready.
Some words about the feature:
with a DDD approach, all repositories would be interfaces, which opens the door to have multiple implementations: usually a Doctrine one and a "in-memory" one.
In one of my project, we use two different kernels in test: the common one, and a
InMemoryKernel
one, which injects in-memory repositories instead of doctrine ones.Most of the factories have the following method, which will store the new object in a property of the in-memory repository:
Beside of this, I'm testing all my "doctrine" and "in-memory" repositories with the exact same class, using an abstract test class and two implementations which only have a
setUp()
method. This way I can be sure both implementations behave the same way and I know I can safely replace my doctrine repositories by the in-memory ones. Resulting in super fast kernel tests!Another very cool benefit is that we do not have to mock repositories anymore! Once the in-memory factories are initialized, we just need to call
$factory->create()
and the object is directly available in the tested code.My current implementation works well, but is very perfectible, mainly because in some cases it is a little bit hard to work with relationships. I really think this is a very nice feature, which should be in Foundry!
About the implementation I have in mind:
First, I'd like this behavior to be globally enabled or disabled with some kind of marker. Not sure which kind of marker: maybe in a first implementation, a simple function like
enable_in_memory()
will suffice. But IMO the best way would be an attribute on the test method or test class.We must introduce an interface for the in memory repositories:
Then, when the option is enabled, we need a way to create in-memory factories. We'd need to hook in the factory creation process, in order to expose only in-memory factories, when the feature is enabled.
This will need a little bit of refactoring because currently, when a factory is not a service, we create it with a static call:
Factory::new()
My suggestion would be to change how
FactoryRegistry
behaves and to create the factory inside of it, if we don't find it in the container.(it would become a....
FactoryFactory
😱 but this name is really awful, I think we can keep the current name)Then, we could extract an interface from the
FactoryRegistry
and decorate it withInMemoryFactoryRegistry
:We also need a way to guess the in-memory repository from the factory's class name. One of the solutions would be to introduce an new attribute
#[AsInMemoryRepository(class: Object::class)]
.And voilà! 🎉 this is all we need as a first step.
Next step would be to to provide a in-memory version of
RepositoryDecorator
because, I'd really like to be able to use things like::findOrCreate()
within the in-memory tests. And thenRepositoryAssertions
should also be needed! But let's keep simple for a first iteration 😅As a bonus, I think we can isolate all the code into a
Zenstruck\Foundry\InMemory
namespace, which will eventually have its own repo in the future.Do you have any thoughts about this?
The text was updated successfully, but these errors were encountered: