deefour/interactor

How to pass data between different Contexts across multiple Interactors?

Leuloch opened this issue ยท 7 comments

Hi!

I'm trying to use the Organize to combine Interactors that have small pieces of business logic, but I cant figure out how to pass data between different contexts.

Using the documentation example for CreateUserContext and CreateVehicleContext, how to pass the new User to the Vehicle Context?

Does it make sense to use a common context for both Interactors?

@Leuloch @dgallinari Thanks for bringing this up. This seems like something I need to think through a bit more. I see room for improving how contexts within a composite context can share data.

I will say though that if both your contexts require a User - for example if your CreateVehicleContext's constructor looks something like this

public function __construct(User $user, $vin, array $attributes)
{
    // ...
}

You could provide the same User instance during instantiation that you do to the CreateUserContext

$user = new User;

$createUser    = new CreateUserContext($user, $request->all());
$createVehicle = new CreateVehicleContext($user, $request->input('vin'), $request->all());

$composite = new RegisterUserContext($createUser, $createVehicle);

The important bit is to realize that when the CreateVehicle interactor runs, the single User instance provided to both contexts will now be a valid, persisted user in the database, having been created during the CreateUser's work cycle.

Thanks!!

Based on your thoughts, I had an idea. When you mentioned about "sharing" data across contexts, I've suddenly foresaw a method "share()" on CompositeContext, responsible for such action.

I will try to implement it and then create a PR for this improvement.

Thanks in advance!
Regards,
Daniel

I've just created the PR.

The scenario where I needed to use this was the following:

  • a AddProductToPartner Organizer
use Deefour\Interactor\Organizer;

class AddProductToPartner extends Organizer
{
    /**
     * Constructor.
     *
     * @param AddProductToPartnerContext $context
     */
    public function __construct(AddProductToPartnerContext $context) {
        parent::__construct($context);
    }

    /**
     * Get Partner and add new product.
     */
    public function organize()
    {
        $viewPartnerInteractor = new ViewPartner($this->getContext(ViewPartnerContext::class));
        $viewPartnerContext    = $viewPartnerInteractor->call();

        /**
         * Shares attribute 'partner' from ViewPartnerContext with CreateProductContext.
         */
        $this->context()->share(
            ViewPartnerContext::class,
            CreateProductContext::class,
            'partner'
        );

        $this->addInteractor(new CreateProduct($this->getContext(CreateProductContext::class)));
    }
}
  • a AddProductToPartnerContext CompositeContext
use Deefour\Interactor\CompositeContext;

class AddProductToPartnerContext extends CompositeContext
{
    /**
    * Constructor
    *
    * @param ViewPartnerContext    $viewPartner
    * @param CreateProductContext $createProduct
    */
    public function __construct(
        ViewPartnerContext $viewPartner,
        CreateProductContext $createProduct
    ) {
        parent::__construct(func_get_args());
    }
}

This worked well for me (as long as I implement share() method as in PR #8 ).

I would like to have your thoughts on this... Is this an expected scenario or I am getting myself away from the original concept of an Organizer? ๐Ÿ˜…

The Rails version of interactor has this to say on organizers:

The organizer passes its context to the interactors that it organizes, one at a time and in order. 
Each interactor may change that context before it's passed along to the next interactor.

So (if I have this correct) if the organizer has some user in its context, the user would be applicable to any interactor declared within the organizer. This PR would bring in some differences (as the organizer does not have a share method that I know about).

I like the idea of share but I also like the idea of explicit shared contexts within organizer interactors. What does everyone think? What is the correct approach?

@dgallinari The issue I have with your example is that when you instantiate the AddProductToPartner organizer, you're passing in a composite context that includes an instance of CreateProductContext.

If your CreateProduct interactor depends on CreateProductContext having a partner attribute, that attribute should be a requirement on CreateProductContext's constructor.

Does that make sense?


I'm wondering if the solution is to abandon the concept of a CompositeContext.

If an Organizer accepted a regular Context, the Organizer::organize() method could be left responsible for the instantiation of each interactor's context.

0687dac is a first attempt at this idea.

See the RegisterUser.php stub file for an example of the new functionality.

The idea is that each interactor resolver passed to enqueue() will receive the organizer's context as well as the context of the previously executed interactor as arguments. Rollbacks and failures should behave the same as before.

0687dac has been released with 2.0.0