9

I'm implementing a service using Slim 4 framework which will pretty much always be returning JSON responses. I'm trying to keep all of the responses uniform with a structure similar to this:

{
    "status": "success",
    "data": {"foo": "bar"} // the actual data relevant to the request
    "messages": []
}

In the most basic format, this is the code that I would need to do to make responses like this:


public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface {
    
    // Do something 

    $response
        ->getBody()
        ->write(json_encode([
            'status' => 'success',
            'data' => [
                'foo' => 'bar'
            ],
            'messages' => [],
        ]));
    return $response
        ->withHeader('Content-Type', 'application/json')
        ->withStatus(200);
}

Right now, I've been using a basic helper class that essentially wraps most of that boilerplate into a few static functions, so I can write a response like this:

public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface {
    
    // Do something 

    $data = ['foo' => 'bar'];

    return APIResponse::success($response, $data)->into();
}

However, now I'm encountering an issue where I'd like to make the responses slightly more complex which would require additional dependencies, such as custom serializer classes. The naive option would be to continue to pass additional dependencies to APIResponse::success($response, $serializer, ..., $data), but that's obviously bad and not a good long-term option.

Another option I thought of would be to make an APIResponseFactory, which would take in any dependencies in the constructor and be filled via PHP-DI. That would be a little cleaner, but every route handler would need to have the factory injected and I'd still need to manually pass $response every time.

return $responseFactory->success($response, $data);

So, what I'm considering now is trying to build a custom class that would implement ResponseInterface, thus allowing me to build in the boilerplate helpers into every request handler automatically. I was looking over the current PSR7 ResponseInterface implementation that's being used in my project, and the code comments mentioned that the class should never be extended, and suggested using a Decorator Pattern. So, this is a basic pseudocode implementation I made for my current idea.

class MyCustomResponse implements ResponseInterface {

    private $serializer; 

    private $actualResponse;

    // any other dependencies

    public function __construct(ResponseInterface $actualResponse, Serializer $serializer /*, other dependencies */) {
        $this->actualResponse = $actualResponse;
        $this->serializer = $serializer;
    }

    // Use this class as a decorator and pass all ResponseInterface calls to the external implementation
    // EDIT: It looks like I can't use `__call` to fulfill the interface, so I'd need to manually define to functions, but you get the gist. 
    public function __call($name, $args) {
        return $this->actualResponse->$name(...$args);
    }

    public function success($data) {
        $this->actualResponse
            ->getBody()
            ->write($this->serializer->serialize([
                'status' => 'success',
                'data' => $data,
                'messages' => [],
            ]));
        $this->actualResponse
            ->withHeader('Content-Type', 'application/json')
            ->withStatus(200);
        return $this;
    }
}

Thus, I would (hopefully) be able to return responses like this:

public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface {
    $data = ['foo' => 'bar'];
    return $response->success($data);
}

My actual questions: Is this the proper way to implement custom helper methods for PSR-7 Response handlers? Is there a better way? Is writing helper functions like this bad practice? PSR-7 interfaces seem to be, for lack of a better description, low-levelish and verbose, which makes me worry that writing a wrapper like this somehow goes against the intent of the standard. Are there any other ways to follow the standard but reduce boilerplate and keep responses uniform?

404 Not Found
  • 3,635
  • 2
  • 28
  • 34
  • To be clear, most of this was simplified for the sake of the post. I am currently using middleware to set the content type. I would prefer to not inject the serializer into every controller, since the pattern I'm using has one class for each route; that would work fine, but it's a lot of redundant typing. An abstract base class might work, but I think I'd still need to add the serializer to the controller constructor, right? – 404 Not Found Nov 15 '21 at 19:02
  • Here's [the `APIResponse` class I have been using](https://github.com/marcusball/virtual-phone-server/blob/f30294941e2d8de43f14ae74e730cdc120b6b131/src/API/APIResponse.php) and referenced at the beginning of the question. – 404 Not Found Nov 15 '21 at 19:05
  • No, it'd be the opposite case in which the child _would_ define it's own constructor to receive other injected dependencies. Like, if the Base class just required the serializer, but a child class required `DependencyA` and `DependencyB` classes, I'd still need to make the child class constructor take all three including the serializer, so I could call `parent::__constructor($serializer)`. – 404 Not Found Nov 16 '21 at 00:20
  • 1
    This was a good question until the end. Your "actual questions" are all asking for opinions. I'd suggest editing to include actual problems you've faced trying to implement this code. – miken32 Nov 29 '21 at 23:17

2 Answers2

2

A 'nice' controller actions return $response->success($data); can be done via

This a bad practice

  • The {"status":"", "data":[], "messages": []} format may change
  • Appearing file, stream, etc responses

Forgot about the ResponseInterface $response argument, how was it done with the array $args argument.

Inject the response factory to each controller, so your actions would look like

public function __invoke(ServerRequestInterface $request): ResponseInterface 
{
    // Do something 

    return $this->responseFactory->createJson($data);
    // OR
    //return $this->responseFactory->createSomethingElse();
}
cetver
  • 11,279
  • 5
  • 36
  • 56
  • A few opinions for my use case: I'm not sure I see how the "bad practices" are applicable. I specifically want to define a *standard* output format; yes, it might change in the future, but if it did, I'd want that format change reflected in **all** responses. I'm not considering file/stream responses, but if I did, I would add different response methods; this is just a stripped down example for the question. This might actually be the best answer, but I still don't like that I would have to manually inject the `responseFactory` into *every* route controller, which is what I'm trying to avoid. – 404 Not Found Nov 28 '21 at 21:50
1

You've written multiple times that you'd like to avoid injecting the dependency for every controller, I don't think you should avoid using dependency injection since it's not that much work load and it comes with a lot of advantages such as:

  • testability You can mock all the dependencies at truly test everything
  • modularity Thus way your class is isolated from others and does not depend on global and not clear dependencies
  • maintainability You have a clear definition of what that class needs to work, and I assure you that's going to be essential a few years from now

A dependency injection container really helps with the amount of code written to apply di.

My controllers usually end up looking something like this:

    class MyController implements RequestHandlerInterface
    {
        private Presenter $presenter;
       
        public function __construct(
            Presenter $presenter
        ) {
            $this->presenter = $presenter;
        }
    
        public function handle(ServerRequestInterface $request): ResponseInterface
        {
            return $this->presenter->present(
                200, 
                [
                    'status' => 'success',
                    'data' => $data,
                    'messages' => [],
                ],
                //other presenter arguments here such as a class instance that defines a standard Metadata object to be included in all Responses
            );
        }

Your presenter can have it's dependencies too and it's all kept clean and tidy.

The Slim 4 routing setup will be something like:

$app->get('/route_name', MyController::class);

This way it's DI Container manner to inject everything and routing page is clean and tidy too!

Davide Bicego
  • 562
  • 2
  • 6
  • 24
  • Yeah, I am using PHP-DI, so I may have to go this route. Basically, the main thing that *feels* wrong to me is that if **every** route will need that dependency injected, it feels like there should be a way to abstract that away so it wouldn't need to be explicitly added to every controller class. I'd still need to manually pass the `$response` to the serializer/`Presenter` for every response too. Conceptually it feels like it's unnecessary duplication, but it's looking like that may be the only option. – 404 Not Found Nov 30 '21 at 23:09