Skip to content

Add AddPathPlugin and BaseUriPlugin #48

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

Merged
merged 2 commits into from
Oct 19, 2016
Merged

Conversation

soullivaneuh
Copy link
Contributor

@soullivaneuh soullivaneuh commented Oct 14, 2016

Q A
Bug fix? yes
New feature? no
BC breaks? no since this is a kind of bug regarding the situation
Deprecations? no
Related tickets N/A
Documentation php-http/documentation#163
License MIT

What's in this PR?

This bugfix permits to keep the base path of an URI on the AddHost plugin.

Why?

With the following PHP code example:

$httpClient = new HttpMethodsClient(
    new PluginClient(HttpClientDiscovery::find(), [
        new AddHostPlugin(UriFactoryDiscovery::find()->createUri('https://logs.domain.com/api')),
        new ErrorPlugin(),
    ]),
    MessageFactoryDiscovery::find()
);
$httpClient->get('/users');

The https://logs.domain.com/users URL will be called instead of /api/users.

The base path should be kept on this plugin.

Checklist

  • Updated CHANGELOG.md to describe BC breaks / deprecations | new feature | bugfix
  • Update the tests

@soullivaneuh soullivaneuh force-pushed the add-host-path branch 2 times, most recently from 1781c76 to 3c94066 Compare October 14, 2016 12:41
@dbu
Copy link
Contributor

dbu commented Oct 15, 2016

as far as i remember, @sagikazarmark had a good argument why this should be a separate plugin. i can't find the place we discussed this anymore. mark would you rembember?

doing this like you do here would be a BC break-ish thing as people that now use a domain with path and rely on the path not being appended would see their applications stop working. and its a new feature, not an actual bugfix so i think we should not do a BC break in a minor version for this.

for the content, question is whether we treat /path/something and path/something the same or only prepend a path when there is no leading /? depending on the scenario, both can make sense.

@sagikazarmark
Copy link
Member

This one? php-http/plugins#42 (comment)

@dbu
Copy link
Contributor

dbu commented Oct 15, 2016

exactly, thanks!

to sum it up:

separate plugin for path:

  • + clearly separated logic
  • + mix and match with replace host or add host
  • - two plugins to configure
  • - unexpected behaviour when specifying path to AddHostPlugin => should we forbid that?

if we would mix it together, would we only add the path if the host is missing, or always even if the host exists? should we check if the path already starts with the configured path and if so, skip adding it again (probably not)?

@soullivaneuh what is your opinion on the other thread? would you agree that we move this to a separate plugin?

@sagikazarmark
Copy link
Member

AddHostPlugin only sets the host, scheme and port, ommits the path at the moment.

@joelwurtz
Copy link
Member

Yes we talk about this and say it must be in a separate plugin like an AddPathPlugin, then we could imagine a third one using these two (like BaseUriPlugin).

@joelwurtz joelwurtz closed this Oct 15, 2016
@joelwurtz joelwurtz reopened this Oct 15, 2016
@soullivaneuh
Copy link
Contributor Author

soullivaneuh commented Oct 17, 2016

doing this like you do here would be a BC break-ish thing as people that now use a domain with path and rely on the path not being appended would see their applications stop working. and its a new feature, not an actual bugfix so i think we should not do a BC break in a minor version for this.

@dbu This is technicaly a BC break, and you know how I'm pernickety with this for Sonata. 😉
But for me it's not really a BC break because who will setup http://domain.com/api on this plugin if the /api append does not work?

But I agree with your point of view about the goal of the plugin, I hesitated for this too.

I'll rework it for a AddPathPlugin then.

then we could imagine a third one using these two (like BaseUriPlugin).

@joelwurtz What do you mean by "using"? I don't see how expect rewrite the logic of both on this class.

@joelwurtz
Copy link
Member

@joelwurtz What do you mean by "using"? I don't see how expect rewrite the logic of both on this class.

Something like this :

class BaseUriPlugin
{  
    private $addHostPlugin;

    private $addPathPlugin;

    public function __construct($options = [])
    {
        // @TODO deal with options
        $this->addHostPlugin = new AddHostPlugin($options);
        $this->addPathPlugin = new AddPathPlugin($options);
    }

    public function handleRequest(RequestInterface $request, callable $next, callable $first)
    {
        $addHostNext = function (Request $request) use ($next, $first) {
             return $this->addHostPlugin->handleRequest($request, $next, $first);
        };

        return $this->addPathPlugin->handleRequest($request, $addHostNext, $first);
    }
}

@dbu
Copy link
Contributor

dbu commented Oct 17, 2016

i like joel's proposal. the coupling of BaseUriPlugin will be a bit tight like this. Maybe use two option arguments, one for host and one for path? that leaks the abstraction but is clean and future compatible

@soullivaneuh
Copy link
Contributor Author

Maybe use two option arguments, one for host and one for path?

My AddPathPlugin does not need options ATM.

@soullivaneuh soullivaneuh force-pushed the add-host-path branch 2 times, most recently from a179c23 to 1b62dc0 Compare October 17, 2016 13:58
@soullivaneuh
Copy link
Contributor Author

Code updated, thanks for reviewing.

@soullivaneuh soullivaneuh changed the title The AddHost plugin should take care of the base path Add AddPathPlugin and BaseUriPlugin Oct 17, 2016
Copy link
Member

@joelwurtz joelwurtz left a comment

Choose a reason for hiding this comment

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

Need to be fixed (see my comments) otherwise LGTM

return $this->addPathPlugin->handleRequest($request, $addHostNext, $first);
}

return $addHostNext;
Copy link
Member

Choose a reason for hiding this comment

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

Should be return $addHostNext($request);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.


$this->beConstructedWith($host, ['replace' => true]);
$this->handleRequest($request, function () {}, function () {});
}
Copy link
Member

Choose a reason for hiding this comment

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

Can you add test when there is no path ? (with my previous comment it should have failed :) )

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

public function __construct(UriInterface $host)
{
if ($host->getPath() === '') {
throw new \LogicException('Host path can not be empty');
Copy link
Member

Choose a reason for hiding this comment

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

cannot

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

/**
* @param UriInterface $host
*/
public function __construct(UriInterface $host)
Copy link
Member

Choose a reason for hiding this comment

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

By the way, what is the advantage of passing an UriInterface instance here instead of a plain string?

Copy link
Contributor

Choose a reason for hiding this comment

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

i guess the advantage is that we have validation. otherwise we would need to discover the uri factory or use parse_url to get the path part and make sure we have a path part.

Copy link
Member

Choose a reason for hiding this comment

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

Do we really? the only with we do with $host is to convert it to a string..

Copy link
Member

Choose a reason for hiding this comment

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

Well, actually we don't need a host here at all. The user just has to pass a path (not a host) and I am not sure that actually needs any validation at all.

Copy link
Contributor

Choose a reason for hiding this comment

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

no, we do $host->getPath() once here to validate, then again where we use it to just get the path part. if we would accept any string, you could create this plugin with "foo bar baz" and we would try to change the request uri to that. or we start to parse urls in here (which might have been validated and parsed before being passed), which is what the Uri model would free us from.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree with @dbu.

This also make an easier argument passing for BaseUriPlugin.

After that, it's as you want. I can pass a string instead if you prefer.

/**
* @param UriInterface $host
*/
public function __construct(UriInterface $host)
Copy link
Member

Choose a reason for hiding this comment

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

Does this need to be a URIInterface? It will always be converted to a string.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Discussed here: #48 (comment)

private $addHostPlugin;

/**
* @var AddPathPlugin
Copy link
Contributor

Choose a reason for hiding this comment

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

this is AddPathPlugin|null

Copy link
Member

Choose a reason for hiding this comment

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

I do not think the docblock here is necessary at all. IDEs should be able to infer the type themselves (the same applies to the $addHostPlugin property).

Copy link
Contributor

Choose a reason for hiding this comment

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

we are using these blocks throughout the code, imho its consistent to keep it. with the |null and a text saying this is the plugin for adding the path and only set if there is a path to add at all, it would be valuable

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

{
$this->addHostPlugin = new AddHostPlugin($host, $hostConfig);

if ($host->getPath()) {
Copy link
Contributor

@dbu dbu Oct 18, 2016

Choose a reason for hiding this comment

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

we should rtrim($path, '/') here. or throw an exception on http://domain.com/ (it makes no sense to prepend just / to a path)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
$request = $request->withUri($request->getUri()
->withPath($this->host->getPath().$request->getUri()->getPath())
Copy link
Contributor

Choose a reason for hiding this comment

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

what about anything ending on /? notably http://domain.com/ but also http://domain.com/path/ - both would end up in double leading slash, which is very likely not what the user wanted.

this is a rather annoying edge case. i think we should somehow handle it (maybe validate and InvalidArgumentException) to avoid wtf moments (some servers will handle the example.com//something paths, others wont). if somebody actually wants to create double slashes, they can write their own plugin, its a rare edgecase.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will add a test for that and handle it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

@soullivaneuh
Copy link
Contributor Author

Requested changes was made.

Except if you want me to change something on https://github.com/php-http/client-common/pull/48/files/2aac11b11bfd025c9d6082e722a1408139ab36f8#diff-44d3f10fc0543e373abc05223f369e01, this PR should be LGTM.

Copy link
Contributor

@dbu dbu left a comment

Choose a reason for hiding this comment

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

went over this and think we should throw an exception rather than eat away offending / characters. and some minor codestyle requests.

@@ -8,7 +8,8 @@
- Fix Emulated Trait to use Http based promise which respect the HttpAsyncClient interface
- Require Httplug 1.1 where we use HTTP specific promises.
- RedirectPlugin: use the full URL instead of the URI to properly keep track of redirects

- Add AddPathPlugin for API URLs with base path
- Combine AddHostPlugin and AddPathPlugin onto BaseUriPlugin
Copy link
Contributor

Choose a reason for hiding this comment

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

Lets say "Add BaseUriPlugin that combines ..."

*/
public function __construct(UriInterface $host)
{
if (rtrim($host->getPath(), '/') === '') {
Copy link
Contributor

Choose a reason for hiding this comment

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

i think i would check '' or last character is '/' and throw an exception when it is. silently fixing things makes harder debugging. after doing example.com/ once or twice, people will learn to use example.com

public function handleRequest(RequestInterface $request, callable $next, callable $first)
{
$request = $request->withUri($request->getUri()
->withPath(str_replace('//', '/', $this->host->getPath().$request->getUri()->getPath()))
Copy link
Contributor

Choose a reason for hiding this comment

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

this feels odd. i would rtrim '/' again. if somebody puts double slashes elsewhere, they probably wanted that. but see above...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we throw an exception like asked in #48 (comment), we don't need to rtrim anything IMHO.

/**
* @var UriInterface
*/
private $host;
Copy link
Contributor

Choose a reason for hiding this comment

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

lets call this $uri, instead of $host. also in the constructor argument.

* @param UriInterface $host
* @param array $hostConfig Config for AddHostPlugin. @see AddHostPlugin::configureOptions
*/
public function __construct(UriInterface $host, array $hostConfig = [])
Copy link
Contributor

Choose a reason for hiding this comment

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

s/$host/$uri/

private $addPathPlugin = null;

/**
* @param UriInterface $host
Copy link
Contributor

Choose a reason for hiding this comment

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

we should say that this must contain a host name and may contain a path that must not end on /

@soullivaneuh soullivaneuh force-pushed the add-host-path branch 2 times, most recently from 36c470d to de9bff6 Compare October 18, 2016 09:43
@soullivaneuh
Copy link
Contributor Author

@dbu All requested changes was made.

@dbu
Copy link
Contributor

dbu commented Oct 18, 2016

LGTM

use Psr\Http\Message\UriInterface;

/**
* Add base path to the request URI. Useful for base API URLs like http://domain.com/api.
Copy link
Member

Choose a reason for hiding this comment

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

Maybe be more clear here:

"Prepend a base path to..."

use Psr\Http\Message\UriInterface;

/**
* This plugin combines the host and path plugins.
Copy link
Member

Choose a reason for hiding this comment

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

Maybe:
This plugin combines the AddHostPlugin and AddPathPlugin.

Copy link
Member

@sagikazarmark sagikazarmark left a comment

Choose a reason for hiding this comment

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

Nice work, thanks!


function it_is_initializable(UriInterface $uri)
{
$uri->getPath()->shouldBeCalled()->willReturn('/api');
Copy link
Member

Choose a reason for hiding this comment

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

I think this could be moved to the let method. That would simplify things.

}

function it_throws_exception_on_empty_path(
UriInterface $host
Copy link
Member

Choose a reason for hiding this comment

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

Why multiline?

}

function it_throws_exception_on_trailing_slash(
UriInterface $host
Copy link
Member

Choose a reason for hiding this comment

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

Why multiline?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, a copy/paste with parameters removal... 😛


function it_is_initializable(UriInterface $uri)
{
$uri->getHost()->shouldBeCalled()->willReturn('example.com');
Copy link
Member

Choose a reason for hiding this comment

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

Same here: can these be moved to let?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll look at this, I'm more familiar with PHPUnit than PHPSpec. 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried this:

    function let(UriInterface $uri)
    {
        $uri->getPath()->shouldBeCalled()->willReturn('/api');

        $this->beConstructedWith($uri);
    }

But got this error:


        Http/Client/Common/Plugin/AddPathPlugin
  47  ✘ throws exception on trailing slash
        some predictions failed:
          Double\UriInterface\P114:
            No calls have been made that match:
              Double\UriInterface\P114->getPath()
            but expected at least one.

        Http/Client/Common/Plugin/AddPathPlugin
  56  ✘ throws exception on empty path
        some predictions failed:
          Double\UriInterface\P116:
            No calls have been made that match:
              Double\UriInterface\P116->getPath()
            but expected at least one.

I don't think this can be refactored on the let method because the return value changes on some cases.

Copy link
Member

@joelwurtz joelwurtz Oct 18, 2016

Choose a reason for hiding this comment

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

AFAIK, let will set default config, so you can set the default input in the let method and override it where it needs (when there is an empty path by example)

@soullivaneuh
Copy link
Contributor Author

@Nyholm @sagikazarmark Requested changes made, except #48 (comment) for the moment.

Copy link
Member

@Nyholm Nyholm left a comment

Choose a reason for hiding this comment

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

Awesome. I'm happy with these changes.

I'll let Mark decide on the PHPSpec comments.

@dbu
Copy link
Contributor

dbu commented Oct 18, 2016

great! there is one thing left to do: the documentation pull request so that these plugins are documented as well. can you please do that?

@soullivaneuh
Copy link
Contributor Author

Sure! Shall I just add two files on this part? https://github.com/php-http/documentation/tree/master/plugins

@Nyholm
Copy link
Member

Nyholm commented Oct 18, 2016

Yes, I've added an issue for that. See php-http/documentation#162

@soullivaneuh
Copy link
Contributor Author

Documentation added: php-http/documentation#163

@dbu dbu merged commit c14fdb6 into php-http:master Oct 19, 2016
@dbu
Copy link
Contributor

dbu commented Oct 19, 2016

thanks @soullivaneuh !

@sagikazarmark
Copy link
Member

Thank you @soullivaneuh

@soullivaneuh soullivaneuh deleted the add-host-path branch November 4, 2016 08:58
@soullivaneuh
Copy link
Contributor Author

Thank you too! Could we please have a new release for this feature? Thanks.

@sagikazarmark
Copy link
Member

@xabbuh
Copy link
Member

xabbuh commented Nov 4, 2016

Shouldn't that have become 1.4.0?

And by the way, could you fix the link to the changelog for the 1.3.0 release? :)

@soullivaneuh
Copy link
Contributor Author

Shouldn't that have become 1.4.0?

I agree, it's a new feature. It should be a new minor if you follow semver ofc.

@sagikazarmark
Copy link
Member

Well, it didn't seem to be that huge change, but you are right.

@sagikazarmark
Copy link
Member

Done

@soullivaneuh
Copy link
Contributor Author

Thank you!

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.

6 participants