SubRequest handling into another route


#1

Hello,

for my app i want to separate the api from two different frontends. The api routes themselves return a json response and i want to call api methods directly from the frontend routes.

to explain better my API routes are for example:

$app->group('/api', function () {
$this->group('/configuration', function () {
    $this->get('[/{id:[0-9]+}]', 'App\Actions\Configuration\GetConfig')->setName('get-config');
    $this->post('[/{id:[0-9]+}]', 'App\Actions\Configuration\AddConfig')->setName('set-config');
    $this->put('[/{id:[0-9]+}]', 'App\Actions\Configuration\UpdateConfig')->setName('update-config');
    $this->delete('[/{id:[0-9]+}]', 'App\Actions\Configuration\DeleteConfig')->setName('delete-config');
});
$this->group('/language', function () {
    $this->get('[/{id:[0-9]+}]', 'App\Actions\Language\GetLanguage')->setName('get-language');
    $this->post('[/{id:[0-9]+}]', 'App\Actions\Language\AddLanguage')->setName('insert-language');
    $this->put('[/{id:[0-9]+}]', 'App\Actions\Language\UpdateLanguage')->setName('update-language');
    $this->delete('[/{id:[0-9]+}]', 'App\Actions\Language\DeleteLanguage')->setName('delete-language');
});
});

And my Application route can be:

$app->get('/language[/{id:[0-9]+}]', function ($request, $response, $args) use ($app) {

// Make a subrequest to the api method
 $res = $app->subRequest('GET', 'api/language',  (!isset($args['id'])) ? '' : $args['id']);
 $json = $res->getBody();
 $language = json_decode($json, true);

    // Sample log message
    $this->logger->info("Slim-Skeleton '/' route");

    // Render index view
    return $this->renderer->render($response, 'index.phtml', ['lang' => $language]);
})->setName('get-api-language');

The problem is that when i call the $app->subrequest method the render renders the subrequest response first and then outputs the index.phtml template populated with the decoded json data.

There is in slim a better method to not outputdirectly the subrequest response into the main application request?


#2

App::subRequest() should not output anything, it will invoke the application and return the response.

Are you echoing anything in the api action? If so replace that with the $response->getBody()->write($..) method


#3

Thank you for your answer.

the controller called is:

<?php

 namespace App\Actions\Language;

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use App\DataMappers\Language\LanguageMapper;

class GetLanguage {

   protected $db;

    public function __construct($container) {
        $this->db = $container->get('db');
    }

    public function __invoke(Request $request, Response $response, $args) {
 
    $mapper = new LanguageMapper($this->db);

    if (isset($args['id'])) {
        $res = $mapper->findById($args['id']);
        if (empty($res)) {
            return $response->withStatus(404)
                            ->withHeader('Content-Type', 'text/html')
                            ->write('Page not found');
        }
    } else {
        $res = $mapper->findAll();
    }

    return json_encode($res);
}
}

The language mapper object makes sample query to database via PDO and returns a JsonSerializable container class, there are not echo in all the api.

forgot to say that I’m on latest slim3.

UPDATE:

If i var_dump($reponse) inside the main route the result is null like this:

$app->get('/language[/{id:[0-9]+}]', function ($request, $response, $args) use ($app) {

// Make a subrequest to the api method
$res = $app->subRequest('GET', 'api/language',  (!isset($args['id'])) ? '' : $args['id']);
$json = $res->getBody();
$language = json_decode($json, true);

// Sample log message
$this->logger->info("Slim-Skeleton '/' route");

// resulting response is null
var_dump($reponse);

// Render index view
return $this->renderer->render($response, 'index.phtml', ['lang' => $language]);
})->setName('get-api-language');

So you are right the subrequest outputs nothing, but $this->renderer->render method seems that catch the subRequest response


#4

You are not returning a Response object

Replace return json_encode($res); with:

$response->getBody()->write(json_encode($res));

// also set content type if you are not doing so elsewhere
$response = $response->withHeader('Content-Type', 'application/json;charset=utf-8')

return $response; // important to return the response object

// or Slim specific:
return $response->withJson($res);

#5

Thank you,
i made the changes, but i get the same result.

you are right, if i make var_dump($reponse) into the main route the result is NULL.

Seems that the return $this->renderer->render($response, catched the subrequest response internally

Instead I find that creating a new reponse object makes the tweak:

$app->get('/language[/{id:[0-9]+}]', function ($request, $response, $args) use ($app) {

// Make a subrequest to the api method
$res = $app->subRequest('GET', 'api/language',  (!isset($args['id'])) ? '' : $args['id']);
$json = $res->getBody();
$language = json_decode($json, true);

// Sample log message
$this->logger->info("Slim-Skeleton '/' route");

 // reset response object
**$reponse = new $response;**

// Render index view
return $this->renderer->render($response, 'index.phtml', ['lang' => $language]);
})->setName('get-api-language');

This is very strange thing, i’m searchning in the render method why the subrequest response is taken.


#6

Ohw I see, that is what’s happening. It seems subRequest uses the request from the container, which is the same one the app itself uses (conainer returns the same instance of response if pulled twice). This architecture is something that bugged Slim before and there are plans to remove the request and response object from the container as they do not belong there.

I think passing a new response instance into the subRequest will also do the job.


#7

Thank you for your help, building an internal client for the api is awesome and slim does the job! :yum:


#8

Hi Joe,
How would I go about passing a new instance of response to the subRequest?

Thanks!


#9

Well, if you look at the signature of the subRequest method you see that the last parameter is a response object. So just create a new instance ResponseInterface and pass it it.

Also it you do not pass a response instance at all it will create a new instance for you.


#10

I’m guessing I may be doing this incorrectly.

use \Psr\Http\Message\ResponseInterface as Response;

$app->post('/login', function ($request, $response, $args) use ($app) {
  $res = new Response;
  $result = $app->subRequest('POST', '/api/v1/login', 'username=' . $username . '&password=' . $password . '', $res);
  $result = json_decode($result->getBody(), TRUE);
});

This throws a PHP error: Cannot instantiate interface Psr\Http\Message\ResponseInterface

Thanks for the help :slight_smile:


#11

I think you’re guessing right :wink:

That error is pretty much self explanatory, you cannot create an object/instance out of an interface.

For your use case I would say just skip passing in the response.

use \Psr\Http\Message\ResponseInterface as Response;

$app->post('/login', function ($request, $response, $args) use ($app) {
  $result = $app->subRequest('POST', '/api/v1/login', "username={$username}&password={$password}");
  $result = json_decode($result->getBody(), TRUE);
});

#12

My reason for asking on this is same as acidvertigo’s, when I do $response->withJson($data), the JSON is displayed since I’m returning the response from the front-end. I need the response from the /api/v1/login endpoint to be a different response than the /login front-end route’s.


#13

Ah, the response body may be shared indeed. Guess you will have to create a new response object manually.

$app->post('/login', function ($request, $response, $args) use ($app) {

  $result = $app->subRequest('POST', '/api/v1/login', "username={$username}&password={$password}", [], [], '',  new \Slim\Http\Response());
  $result = json_decode($result->getBody(), TRUE);
});

#14

Thanks! That fixed it.


#15

@ML88, just a heads up that subrequest will likely go away with Slim v4.


#16

Looking at those links, I don’t see any discussion on why this is going away or what(if anything) it is being replaced with. Any idea on why its going away or if its being replaced with something?


#17

I don’t know. @geggleto might be able to provide some insight.


#18

We are removing subRequest as it does not fit well with proper code design.

In instances where one might need a “sub” request, it is almost always the wrong design choice to resend an HTTP Request through Slim again. Typically we remove features that are not considered good practice with every Major release [ Slim 2 => 3 we removed service location … as an example ].


How to invoke a route "manually"
What is the proper way to run an integration test for Slim 3 and Phpunit?