- Sponsor
-
Notifications
You must be signed in to change notification settings - Fork 134
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
Add support for AsPipeline concern #304
base: main
Are you sure you want to change the base?
Add support for AsPipeline concern #304
Conversation
This is awesome @stevenmaguire, thanks for your work on this. How come the |
Do you mean in the |
Thanks. That's what I thought you were asking about. Yes, I clocked that implementation detail but originally did not include it because if that method was not available, the Pipeline resolution chain would be broken anyway. I'm not sure how someone would configure things in such a way where the Decorator was in play without also having the trait, which furnishes a |
Nope, but in general you should match how a package works. Consistency is important. Hopefully @lorisleiva chimes in here and guides us/makes changes. |
I tend to agree with that, but not as a dogmatic approach. If the code path can't be accessed or exercised with a test (that replicates what a consuming project could do), it shouldn't be included just because another class in the project does it. I'm still scratching my head on how I could exercise that code path, even though I've now added it. Do you have a suggestion on how that might be exercised with a test? |
Totally agree, mine was only a suggestion to be considered. If it isn't right then we shouldn't do it.
Not off the top of my head, but this was only a quick review. If it's dead code path then it def shouldn't be included, but I know as a user of this package I would expect having only a I'll take a look later when I have a few moments. Thanks again for your work on this, it's awesome. |
I just tried to test some use cases that would exercise that code path and was unable to get there. I tried a pipe class which explicitly used another trait from the package but not I tried a generic pipe class and naturally, it did not get there. In the meantime, I've removed the fallback code path from the decorator. I would love a review from @lorisleiva or anyone else with close working knowledge of this package. At the moment, there are two main test cases that are passing. |
Hey guys, thanks for this! I'll have a proper look at it over the weekend and read all your comments. On little thing I've noticed on a first quick look is that the Sorry if thats mentioned in your conversation though, as I said I'll have proper read through everything soon. |
Thanks @lorisleiva! Regarding the /**
* Typical pipeline behavior expects two things:
*
* 1) The pipe class to expect a single incoming parameter (along with
* a closure) and single return value.
* 2) The pipe class to be aware of the next closure and determine what
* should be passed into the next pipe.
*
* Because of these expectations, this behavior is asserting two opinions:
*
* 1) Regardless of the number of parameters provided to the asPipeline
* method implemented here, only the first will be supplied to the
* invoked Action.
* 2) If the invoked Action does not return anything, then the next
* closure will be supplied the same parameter. However, if the
* invoked action does return a non-null value, that value will
* be supplied to the next closure.
*
* Also, this logic is implemented in the trait rather than the decorator
* to afford some flexibility to consuming projects, should the wish to
* implement their own logic in their Action classes directly.
*/ Basically, the whole value of the concern here is that you (the consuming package) don't need to worry about handling the Pipeline's expectations around the callback closure, this concern will take care of it for you. The opinionated logic that is now here in the PR should be furnished by the package somewhere. If that is the case, then furnishing it in the trait leaves it accessible to consuming projects to override the default behavior if they please. Without this opinionated logic in the package it will be left to the consuming project to implement every time and if they don't and it falls back to the The demonstrate, if there exists an Action that is capable of doing everything else this package offers, it would look something like this: class PublishPost
{
use AsAction;
public function handle(Post $post): void
{
// do the things that publish the post.
}
} That is simple and pure and will work for all the existing use cases. But, it alone is not compatible with Pipeline. Here is what would be needed to make it compatible with Pipeline: class PublishPost
{
use AsAction;
public function handle(Post $post, ?Closure $next = null): mixed
{
// do the things that publish the post.
if ($next) {
return $next($post);
}
}
} Pipeline expects that the closure will never be null and the method will return the result of invoking that closure. In this example, I made the obvious assumption that since the Closure won't be supplied for other use cases - like So, the So while this may not be an obvious match for the style and mental model here, I think there is a reasonable justification to consider the deviation in order to add more value and convenience. At the end of the day, I am very new to this package and if you feel strongly about moving this logic to the decorator - or somewhere else 🤔 - I'm all ears. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there is a misalignment on how this package handles AsX
decorators and this PR.
You are saying that, in order to make the handle
method "compatible" with Pipeline one would have to do the following:
class PublishPost
{
use AsAction;
public function handle(Post $post, ?Closure $next = null): mixed
{
// Do the things that publish the post...
if ($next) {
return $next($post);
}
}
}
I disagree.
To illustrate this, let's see how AsController
handle this situation because, it too, has some specific requirements — e.g. route model bindings.
Here's a simple example that uses an action as a controller to publish a post.
class PublishPost
{
use AsAction;
public function handle(Post $post, PublishPostRequest $request): Response
{
$request->validate();
// Do the things that publish the post...
return redirect()->route('posts.published', [$post]);
}
}
This works but because we're using the handle
function, we are "locking" ourselves to only use this action as a controller. Sometimes that's what we want, but most of the time, you want to extract the "main logic" of your action in the handle
method. That way, you can use asX
functions as adapters to plug in your action in various places in the framework.
To improve on the previous design, we would need to use the handle
method exclusively for the "domain logic" of our action, and refer to it inside the asController
method whose responsibility is purely to make this action accessible in web/api routes. Here's our updated example.
class PublishPost
{
use AsAction;
public function handle(Post $post): void
{
// Do the things that publish the post...
}
public function asController(Post $post, PublishPostRequest $request): Response
{
$request->validate();
$this->handle($post); // Notice how we delegate to the domain logic here.
return redirect()->route('posts.published', [$post]);
}
}
Now, with that in mind, how would you make this action also available as a pipeline? With your current design, we cannot do it anymore because you are forcing the handle
method to be pipeline-specific. However, if we follow the same pattern as controllers, we can add a new asPipeline
method that again delegates the handle
method for the domain logic.
Which gives us the following code:
class PublishPost
{
use AsAction;
public function handle(Post $post): void
{
// Do the things that publish the post...
}
public function asController(Post $post, PublishPostRequest $request): Response
{
$request->validate();
$this->handle($post);
return redirect()->route('posts.published', [$post]);
}
public function asPipeline(Post $post, ?Closure $next = null): mixed
{
$this->handle($post);
if ($next) {
return $next($post);
}
}
}
And just like that an action can be used as a pipeline and as whatever else the user needs. And if the user decides they only want this action as a pipeline, they can use the handle
method as a fallback for the pipeline decorator (just like we did initially with the controller) and we end up with your original design which also works.
This is my main design change request on this PR. I wouldn't feel confortable merging something that deviates from the mental model of the whole package.
An additional smaller concern I have is the signature (Post $post, ?Closure $next = null): mixed
. Particularly the input/output selection of the pipeline. If you could add more tests showing that a single action can accept multiple different inputs and return multiple different outputs without the usage of mixed
, I'd feel more confortable with the function signature. 🙏
src/Concerns/AsPipeline.php
Outdated
$passable = array_shift($arguments); | ||
$closure = array_pop($arguments); | ||
|
||
return $closure($this->handle($passable) ?? $passable); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code should live in the decorator.
src/Decorators/PipelineDecorator.php
Outdated
if ($this->hasMethod('asPipeline')) { | ||
return $this->resolveAndCallMethod('asPipeline', $arguments); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code should resolve the passable, closure, etc. and then pass it to the asPipeline
function or fallback to passing it to the handle
function.
@lorisleiva thank you very much for the detailed and thorough feedback. I appreciate it! I think we can make something work here, especially within the constraints of the explanation you provided. I've pushed up an update that - I think - puts this closer to what you might consider acceptable. I'm not afraid of another round of refinement if needed. I gleaned a few of assumed truths from your feedback that were most productive in the latest revision. I want to put them next to numbers so it might be easier to confront and/or refine each assumed truth independently, if needed.
I did add more test cases here to try to exercise these assumed truths relative to the Pipeline concern. Here is the Pest output for that:
Hopefully between those Pest test cases and the assumed truths listed above, the current state of the PR is more clearly aligned. Please do let me know if I am still missing something. |
There we go, great work @stevenmaguire, that's what I meant by "copy the patterns" in a package. Now it looks like all the other action types. |
@lorisleiva do you have any further feedback here given the latest changes? |
why not merge ? cool feature to have |
You can always composer patch this in if you want it now. |
really need this, any eta? |
Due to the complexity this is a PR only @lorisleiva should merge as I am unfamiliar with it. |
Hey guys, I've not come back to you yet because, at first glance, there are still a few smaller design decisions I'm not 100% sure about. I need to take the time to dig properly into the changes and see if there are a few tweak we can do to mitigate this. Rest assured, this will end up being merged as I think it is a valuable feature. However, this is a fairly large addition to an otherwise stable package and I don't want to rush this. Hope you understand. That being said, I'll try and allocate some time this weekend for this PR. P.S.: It looks like CI isn't passing. |
Thanks for the update @lorisleiva. I'll wait for some more specific and granular feedback before making further changes here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay I commented all my thoughts on the current design. It looks like a lot but actually we're much closer to merging point than I thought we were after reflection haha.
Let me know what you think about my comments and do another round of review afterwards.
Thanks for the continued review and feedback here. Refining and solidifying the functional and style requirements is an important process. Please review the latest changes that should be closer in line with the evolving expectations for this feature addition. |
We'll have to wait until the CI workflow is approved again to see how the latest changes are performing. |
This is so slick now now @stevenmaguire great work! |
Thanks for approving the CI runs. There are two failing builds and I'm scratching my head on one of them. If you have some thoughts, please chime in. P8.4 - L^11.0 is failing with the error message:
This is the concern I was mentioning in this thread of the feedback: #304 (comment) But it is curious that it is only an issue with 8.4. I can reproduce this issue locally on 8.4. Laravel's Pipeline handling logic is detecting the handle method on the Action class - the one that expects two parameters in this failing test case - and is invoking that method instead of the __invoke method on the decorator. Furnishing a handle method on the decorator (like a previous iteration of this solution) does not change this behavior. After poking at it for a while, it seems - and I could be wrong - that the P8.4 - L^10.0 is failing with the error message:
This one is a head scratcher. Could there be something wrong with the autoloading of the Laravel framework in this specific environment? |
It seems that the Backtrace frame matching is different between <8.4 and 8.4. Below is the value of
Adding this new pattern to the frame matching gets over that hump but is a little smelly. A couple of other observations from this debugging...
New changes are pushed up and ready for review and CI. Regarding Number 3 above, this feels like a ServiceProvider timing issue that could be overcome (maybe?) but I am not super familiar with that area of the package yet. Perhaps there are some ideas on whether or not that is something we should expect to overcome? Or we can simply assert that this behavior is ONLY expected to work when using the This is obviously failing to match the frame match logic in the PipelineDesignPattern because the resolution is happening outside of an expected invocation scope. I only worry about this from a DevExp perspective - managing expectations around using an Action class in Pipeline and it being different (more restrained). Is this something that the docs alone can manage? Should we provide some code-based feedback that might be noticed during development? To spare you digging into the test cases, here is an example of what works perfectly: it('can run as a pipe in a pipeline, with an explicit asPipeline method', function () {
$passable = Pipeline::send(new PipelinePassable)
->through([
AsPipelineTest::class,
AsPipelineTest::class,
AsPipelineTest::class,
AsPipelineTest::class,
])
->thenReturn();
expect(is_a($passable, PipelinePassable::class))->toBe(true);
expect($passable->count)->toBe(4);
}); Here is an example of what gives the impression of working but actually is not: it('can run as a pipe in a pipeline with only one explicit container resolved instance at the bottom of the stack', function () {
$passable = Pipeline::send(new PipelinePassable)
->through([
AsPipelineTest::class, // implicit container resolved instance
app()->make(AsPipelineTest::class), // explicit container resolved instance
])
->thenReturn();
expect(is_a($passable, PipelinePassable::class))->toBe(true);
expect($passable->count)->toBe(2);
}); Here are two examples of failing behavior: it('cannot run as a pipe in a pipeline with an explicit container resolved instance in the middle of the stack', function () {
$passable = Pipeline::send(new PipelinePassable)
->through([
AsPipelineTest::class, // implicit container resolved instance
app()->make(AsPipelineTest::class), // explicit container resolved instance
AsPipelineTest::class, // implicit container resolved instance
AsPipelineTest::class, // implicit container resolved instance
])
->thenReturn();
expect(is_a($passable, PipelinePassable::class))->toBe(true);
expect($passable->count)->toBe(2); // 4 is the ideal count here
});
it('cannot run as a pipe in a pipeline as an standalone instance', function () {
$passable = Pipeline::send(new PipelinePassable)
->through([
new AsPipelineTest, // standalone instance
AsPipelineTest::class, // implicit container resolved instance
app()->make(AsPipelineTest::class), // explicit container resolved instance
])
->thenReturn();
expect(is_null($passable))->toBe(true);
}); |
As discussed in Feature Request: Support for Illuminate Pipelines #279 there seems to be some interest and appetite in allowing our Action classes to function well within a Pipeline workflow, in addition to all the other already support Laravel conventions.
This PR aims to do that.
The main thing to note are the two opinions being asserted by the new
AsPipeline
trait. An explanation and justification for those opinions are captured in a code comment in the trait file.If there are any critiques or comments, please share them. I am happy to collaborate to get this to a production-ready and mergable state.
Fixes #279