Skip to content

Case of self-referencing fixtures #502

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

Closed
theofidry opened this issue Aug 28, 2016 · 2 comments · Fixed by #506
Closed

Case of self-referencing fixtures #502

theofidry opened this issue Aug 28, 2016 · 2 comments · Fixed by #506
Labels
Milestone

Comments

@theofidry
Copy link
Member

Example:

Dummy:
  dummy{1..10}:
    relatedDummy: '3x @dummy*'

This, in 2.x, will somehow works. Because in 2.x, all the fixtures are first instantiated and then populated, so @dummy* will always find a match. There is however two problems with that:

  • if relatedDummy is no longer set via a setter but in the constructor, this will fail as no instance matching @dummy* will be found:
Dummy:
  dummy{1..10}:
    __construct: ['3x @dummy*']
  • if Dummy is there is immutability involved somewhere this will break. Examples:
class Dummy
{
  // ...

  public functions setRelatedDummy(Dummy $dummy)
  {
    $this->relatedDummy = clone $dummy;
  }
}

Or maybe a more practical example (the one above is really a bit weird):

ImmutableDummy:
  immutable_dummy{1..10}:
    # ...

Dummy:
  dummy{1..10}:
    relatedDummy: ['3x @immutable_dummy*']

If ImmutableDummy is really immutable then any modification on it (for example in the optional method calls step cf. #388), any modification made on the instance won't affect the injected instances.

This problem is in 2.x but I'm facing the same in 3.x. I changed the strategy in 3.x to generate (instantiate + populate + do optional calls) on an instance basis. This allows to make use of recursion and basically allow to make the loading of the fixtures independent of the order in which the fixtures have been declared to solve issues such as #257. But then that means I'm encountering the same issue as in the second example (dummies injected in the constructor) for:

Dummy:
  dummy{1..10}:
    relatedDummy: '3x @dummy*'

A solution I see is instead of going through all the fixtures randomly, try to play it smart: generate the fixtures without a population step first (makes sure all immutable structure are instantiated, as immutable objects won't have this "population" step). Then try to instantiate all fixtures without any constructor (this makes sure they won't be injected an incomplete object which we will have no means to fix after). Try to populate those objects and then generate one by one the fixtures left. A safety could be added to make so that if generating a fixture fails, we skip it and retry it later.

But honestly this is quite a heavy-engineered solution and I'm not sure it's worth it. Sure it will break a few things, some people won't be happy, but it's weird case that already don't work in all cases and creates more problem than anything else.

@tshelburne WDYT? I must admit I'm a bit upset with this issue as the time of writing so don't mind me if I'm a bit negative. But I'm curious if you have an alternative solution in mind or if you're ok to drop it for 3.x.

@theofidry theofidry added this to the 3.x milestone Aug 28, 2016
@tshelburne
Copy link
Collaborator

I don't think we can drop support for it altogether - that would be taking a pretty aggressive stance on what we feel is an acceptable object structure. So, given that, we can either come up with a new syntax / suggested approach for dealing with it (something like a second-pass reconciliation step) or we can engineer to make it work. I lean towards making it work in the cases it has been working, and throwing up a flag in the cases you've recognized where it won't work.

Personally, I feel like your description of generating all the fixtures and ordering their build steps intelligently is the right move - it's easy to imagine in the future wanting to be able to manipulate the order for any number of reasons (process, performance, etc.). Additionally, this would lay groundwork for allowing customization at the user level, if it ever went there. Given the engineering of the library at this point, this doesn't seem over-engineered to me.

I think we can save the "skip and retry" step for a later milestone - it's a useful feature, but we're talking about edge cases at that point.

So, to reiterate, I would suggest:

  1. Support the existing working syntax
  2. Throw up a flag when one of your known issues appears
  3. Put some effort into creating an intelligently ordered process around instance building

One thing you also might consider is changing the population portion to support lambda population - basically, rather than passing an instance of the reference, you pass a function that returns an instance of the object. Hidden behind that function can be a simple random item in a list, or a process that builds a not-yet-handled object. This strikes me as a reliable approach, but is definitely more conceptually "stressful".

@theofidry
Copy link
Member Author

After sleeping on it, it's clear that this is the way that makes more sense. There is only one scenario I can think of where it would fail, it would be the one where an entity has a clone in a setter. I have no easy way to detect that, but I think it is fair to not expect alice to handle this case. Maybe in the future, we can add a flag to specify that entity X should be generated in one go to avoid this situation, i.e. let the user solve this issue on a case by case basis, as it has strong limitations and would break things for too many people to be switched on as the default.

That said I must say I was surprised that:

Dummy:
  dummy0:
    relatedDummy: '@dummy*'

works. Thinking about it, this is perfectly valid, but it was a scenario I did not account for and that confused me.

theofidry added a commit to theofidry/alice that referenced this issue Aug 30, 2016
Instead of trying to generate each fixture one by one in one go, the process is done in two passes. During the first pass, the objects are simply instantiated. During the second pass, they are hydrated and the method calls are made.

Closes nelmio#502
theofidry added a commit to theofidry/alice that referenced this issue Aug 30, 2016
Instead of trying to generate each fixture one by one in one go, the process is done in two passes. During the first pass, the objects are simply instantiated. During the second pass, they are hydrated and the method calls are made.

Closes nelmio#502
theofidry added a commit that referenced this issue Aug 30, 2016
Instead of trying to generate each fixture one by one in one go, the process is done in two passes. During the first pass, the objects are simply instantiated. During the second pass, they are hydrated and the method calls are made.

Closes #502
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants