How to get the base url Programatically in zend expressive?

expressive
#1

I am working on an API application that will be run in different domains: http://example.com/, http://sub.example.com/, http://example-another.com/. Part of the API responses needs to send out its base_url. So I am trying to find a way to dynamically collect the base_url and add it to my response.

I have a factory to initiate the action handler as follows:

class TestHandlerFactory
{
    public function __invoke(ContainerInterface $container) : TestHandler
    {
        
        return new TestHandler();
    }
}

Then my action handler is as follows:

class TestHandler implements RequestHandlerInterface
{
    public function __construct()
    {
        ...
    }
    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        
       ...
    }
}

I am new to Zend world and I found the https://github.com/zendframework/zend-http/blob/master/src/PhpEnvironment/Request.php probably a potential solution of my problem. However, I do not how to get that PHP-Environment object (or any other object that help me grab the base url) in the factory or handler class.

#2

Expressive uses PSR-7, which is a very different abstraction than zend-http. By default, we use Diactoros, which is our PSR-7 implementation (and the one in widest use in the PHP ecosystem).

However, to answer your question more directly, PSR-7 is neither here nor there. What you need in this case is the ServerUrlHelper. Compose this helper in your handler:

use Zend\Expressive\Helper\ServerUrlHelper;

class TestHandler implements RequestHandlerInterface
{
    private $serverUrlHelper;

    public function __construct(ServerUrlHelper $serverUrlHelper)
    {
        $this->serverUrlHelper = $serverUrlHelper;
    }

    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        $baseUrl = $this->serverUrlHelper->generate();
        // ...
    }
}

and then update your factory to inject it:

use Zend\Expressive\Helper\ServerUrlHelper;

class TestHandlerFactory
{
    public function __invoke(ContainerInterface $container)
    {
        return new TestHandler($container->get(ServerUrlHelper::class));
    }
}

By default, we place the ServerUrlHelperMiddleware in the application pipeline, and that middleware will inspect the request to get its Uri instance, and push that into the ServerUrlHelper, so that the above will correctly give you the base URL (which will include the scheme, host, and, if on a non-standard port, the port.

#3

Thanks for your reply. This one is giving me the full url, not the base url. For example: i have a route http://sub.domain.com/install where I need to send out the base url (http://sub.domain.com/). The generate() is including the /install in outs output. Is there any way I get get the base_url directly. I can certainly do the string operations, but I am kin to know the right way, if there is one.

#4

Can you show me how you’re calling generate(), please?

If there is a URI instance present in the ServerUrlHelper, it uses that instance to generate the URL string. The logic is such that if you do not pass an argument to the generate() method, or an empty string, it will use the full URI instance, including path, to generate the string URL.

The way around it is to pass a single slash to it: $url = $this->serverUrl->generate('/');

Doing so will give you the scheme + authority only, which, based on your original post, is what you want.

#5

Thanks,
The workaround works well. My application is currently setup in the root of the domain. Will have to test how it behavs when i will have to set it up in the subfolder. But thanks a lot.

#6

That’s interesting… so is ServerUrlHelper sort of an out-of-band data bridge between different stages in the pipeline? Is it a singleton (ie. shared instance)?

#7

No - it needs to be injected anywhere it’s used.

In the default pipeline, we include the ServerUrlHelperMiddleware, which injects the ServerUrlHelper with the current request - which is why you can access it within handlers and other middleware.

#8

I think we’re saying the same thing, but I want to make sure I understand.

In ServerUrlMiddlewareFactory, there’s:

return new ServerUrlMiddleware($container->get(ServerUrlHelper::class));

So when a user’s middleware wants access to the URL, when they $container->get(ServerUrlHelper::class) in their factory, they’re getting the same instance of ServerUrlHelper, right? That’s the way the user’s middleware communicates with ServerUrlMiddleware - via a shared instance of ServerUrlHelper?

That’s what I meant by “data bridge”.

#9

Yes - this is how PSR-11-compatible DI containers work.

#10

Two successive calls to get with the same identifier SHOULD return the same value. However, depending on the implementor design and/or user configuration, different values might be returned, so user SHOULD NOT rely on getting the same value on 2 successive calls.

But, regardless, ServiceManager does share instances, by default, as you’d know of course. I just think using that mechanism as a singleton, and as a way of sharing state, is interesting. Hadn’t seen that in my ZF/mvc use.

#11

Except that it isn’t a singleton, because you can instantiate it more than once. A traditional singleton limits the number of instances that can exist at any given time to exactly one.

It seems like a singleton, because you’re getting the same instance everywhere. But the reason is because you’re using dependency injection, and the dependency injection container is caching instantiated services for re-use. This allows any state changes in a service to propagate to all consumers of that service.

Interestingly, this latter can and is problematic when using a long-running process, such as an async web server (e.g., Swoole, AMP, ReactPHP). In those cases, state changes in services mean every parallel request or subsequent request now has those changes, even if they are irrelevant for that request. As such, we’ve been working on features such as this one to mitigate state changes in things such as the UrlHelper.

#12

Cool, thanks for the dialog mwop. All clear. I’ve learned a few things (as usual).