Skip to content
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

Map path prefixes to middleware stacks #7

Closed
wants to merge 5 commits into from
Closed

Map path prefixes to middleware stacks #7

wants to merge 5 commits into from

Conversation

franzliedke
Copy link

As proposed in #6.

Please have a look at the tests - they describe the intended feature set quite well.

composer.json Outdated
@@ -20,7 +20,9 @@
},
"require-dev": {
"eloquent/phony-phpunit": "^3.0",
"phpunit/phpunit": "^6.0"
"league/container": "^2.0",
"phpunit/phpunit": "^6.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to require phpunit, phony-phpunit always requires it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's been there before. Do you want me to remove it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doh, of course it was. No need to change anything, composer.json changes should just be reverted.

src/Broker.php Outdated
return $this;
}

public function resolve($middleware): Middleware
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like this being a public method. What are we gaining by having an interface for resolution, as opposed to a shared trait?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure. I was thinking Interface Segregation Principle and "freeing" the RequestHandler (and now the PathDispatcher as well) from having to know about containers...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that actually helps here. The ContainerInterface is a more widely used standard and offers ISP without having to define a local interface that (effectively) does the exact same thing.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine. So I'll just duplicate the three lines of resolve() both in the RequestHandler (where they originally come from) and the PathDispatcher class?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Define a trait that is used by both classes. DRY.


class PathBrokerTest extends TestCase
{
use CanMock;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see no reference to the implementation of this trait.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am calling $this->process().

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh you're reusing an existing trait. Been a while since I worked on this. 😉

// Execute
$this->broker->paths([
'/foo' => new stubs\ReturnsGivenString('something'),
'/bar' => stubs\ReturnsGivenString::class,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use phony for mocking instead of creating stubs.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I am not mocking anything. (I don't care whether and how the things are called, I just check for the expected results.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not necessary to use Phony call checks, I just don't want stubs created when a mock implementation works equally well without adding more files.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, it's your call. Will change this soon-ish.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. I find it super painful having to mock a middleware that returns a mocked response with a mocked body. Here is what I came up with... Am I missing something?

    /**
     * @return \Eloquent\Phony\Mock\Handle\InstanceHandle
     */
    private function mockMiddlewareReturning(string $responseBody)
    {
        $stream = Phony::mock(StreamInterface::class);
        $stream->__toString->returns($responseBody);
        
        $response = Phony::mock(ResponseInterface::class);
        $response->getBody->returns($stream);
        
        $middleware = Phony::mock(MiddlewareInterface::class);
        $middleware->process->returns($response);

        return $middleware;
    }

This is why I like stubs: they are real implementations, and usually super straight-forward.

If the only downside is having one more file in the test code, is that really a downside? (The stub is small enough to be put into the same file, if you prefer that...)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always an option to use an anonymous class if the mocking is too much work. Which I see is already being done in some places.

I would also argue that for many of these tests using Phony mocks and call checks would provide better overall testing because the expectations inside the middleware should remain consistent. For instance in PathPrefixingHandler the expectation is that UriInterface is going to be manipulated. The test should reflect this, rather than testing the output the middleware.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I don't want to test how the PathDispatcher does its work, I just wan't to test that it does it. That means two things:

1.) Ensure it dispatches to the correct middleware. I do this by comparing the expected responses, depending on which request I pipe through the broker.
2.) Ensure that it provides a manipulated request object to the middleware. I do this by returning the path (which is the part that should be manipulated) in the middleware response so that I can verify on the "outside" (a.k.a. from the result of the operation).

Mocking and call checks would couple me to a specific implementation, as far as I can tell, and that's what I wanted to avoid. Can you provide an example of how you would mock this without these downsides?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't you want to test how it works? The internals are more important than the output here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because (in the tests) I don't care about how it does its job. There are a thousand ways to implement that. What exactly would you like to see mocked?

composer.json Outdated
@@ -20,7 +20,9 @@
},
"require-dev": {
"eloquent/phony-phpunit": "^3.0",
"phpunit/phpunit": "^6.0"
"league/container": "^2.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like testing with real implementations (to show real example usage). I can change to use a mock / stub if you want.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So... okay to keep? Not 100% sure how to interpret your 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, remove it please.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay.

composer.json Outdated
@@ -20,7 +20,9 @@
},
"require-dev": {
"eloquent/phony-phpunit": "^3.0",
"phpunit/phpunit": "^6.0"
"league/container": "^2.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, remove it please.


class PathBrokerTest extends TestCase
{
use CanMock;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh you're reusing an existing trait. Been a while since I worked on this. 😉

src/Broker.php Outdated
return $this;
}

public function resolve($middleware): Middleware
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that actually helps here. The ContainerInterface is a more widely used standard and offers ISP without having to define a local interface that (effectively) does the exact same thing.

return $handler->handle($request);
}

private function unprefixedRequest(Request $request, $prefix): Request
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

string $prefix ?

);
}

private function prefixedHandler(Handler $handler, $prefix): Handler
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

string $prefix?

/** @var string */
private $prefix;

public function __construct(RequestHandlerInterface $wrapped, $prefix)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

string $prefix ?

// Execute
$this->broker->paths([
'/foo' => new stubs\ReturnsGivenString('something'),
'/bar' => stubs\ReturnsGivenString::class,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always an option to use an anonymous class if the mocking is too much work. Which I see is already being done in some places.

I would also argue that for many of these tests using Phony mocks and call checks would provide better overall testing because the expectations inside the middleware should remain consistent. For instance in PathPrefixingHandler the expectation is that UriInterface is going to be manipulated. The test should reflect this, rather than testing the output the middleware.

$uri->withPath(
substr($uri->getPath(), strlen($prefix))
)
);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shadowhand How would you test this method works correctly, without passing in a real Request instance?

@franzliedke
Copy link
Author

@shadowhand I have taken care of the requested changes in the source code.

Regarding the tests: I have spent more than an hour trying to test this in a useful way using stubs only, and did not succeed. Right now, we have green (and, IMO, useful) tests. Can I leave this part to you, if it is important to you?

@franzliedke
Copy link
Author

Hmm, you know what... if you are unsure about this, I can just add the PathDispatcher to my user-land code. Now that I have written it anyway.

No hard feelings either way. 😉

@shadowhand
Copy link
Contributor

Don't worry, I'll get to this very soon. Just had to finish out my work week. Expect a release with it by Monday.

@franzliedke
Copy link
Author

No pressure.

@franzliedke
Copy link
Author

Another idea: Hey @oscarotero, would you be interested in accepting such a middleware (see proposed functionality in issue #6) into your middlewares organization?

@oscarotero
Copy link

@franzliedke That's a feature that many dispatchers provide (stratigility, middleland and now broker). But if you feel that it can be more useful in a middleware format, I do not feel bad.

@franzliedke
Copy link
Author

@oscarotero As far as I can tell, Middleland does not strip the path from the request object. Broker does not (yet) have the implementation. That leaves us with Stratigility, which I did not want to use, because I don't (yet) want to drop PHP 7.0 support.

Hence, I suggested to put this into a generic, reusable package instead. Should I create my own repo first and then submit it for inclusion into your organization?

@oscarotero
Copy link

@franzliedke Yes, you're right. Middleland does not strip the path prefix, but it can be done with middlewares/base-path. That brings more flexibility because you can choose to strip the prefix to some paths but not in others.

Anyway, If you want to create the package, you can use this template and then move it to the middlewares organization.

@franzliedke
Copy link
Author

@oscarotero Please have a look at the repository and let me know whether you consider this for inclusion. (And also if there is some other, more official path - or route, eh? - where I should send my, uhm, request.)

@shadowhand
Copy link
Contributor

@franzliedke would the proposed middleware replace this PR?

@franzliedke
Copy link
Author

Yep. (Although it currently lacks one thing in comparison to this PR, which is lazy evaluation of prefixed middlewares, e.g. from the container.)

@oscarotero
Copy link

@franzliedke Great. I left you some suggestions here: middlewares/base-path-router#1

@shadowhand
Copy link
Contributor

@franzliedke I really appreciate the work you did here. In the interest of having the most widely usable solution, I am going to close this out in favor of the middleware, which is a universal solution.

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

Successfully merging this pull request may close these issues.

3 participants