zendframework / zend-diactoros

PSR-7 HTTP Message implementation
BSD 3-Clause "New" or "Revised" License
1.55k stars 152 forks source link

Add DownloadResponse Class #361

Open settermjd opened 5 years ago

settermjd commented 5 years ago

Why is the new feature needed? What purpose does it serve?

After doing a bit of searching, I didn't find a class that would send a CSV response. There were the Text, JSON, HTML, Redirect, XML, and Empty response classes, but nothing specific to CSV. So I created this PR to add a CSV response class, which can send both plain CSV text as well as a response that will be interpreted by the client as a downloadable file.

How will users use the new feature?

Users can use the CSV response class very similarly to how they use the existing response classes. The only difference is that if they supply a file name as the third parameter to the constructor, then a download response will be sent, not a textual response.

settermjd commented 5 years ago

Sorry that I let this slip. I'll get it finalised this week, all being well.

settermjd commented 5 years ago

I like the idea of this a lot. However, I would argue we need two things here:

* A general `DownloadResponse` for specifying file downloads. This could build on your provided `DownloadResponseTrait`. This should allow for either specifying string content as the download OR a filename; the latter is useful, as it can prevent the need for having the full file contents in memory at once.

* Three different constructors for the `CsvResponse`:

  * One which accepts a CSV-formatted string to use.
  * One which accepts a CSV file to use.
  * One which accepts an array of arrays to use, as well as (optionally) a delimiter character and enclosure character. This would build the CSV string.

You can do multiple constructors by using static constuctor methods:

* `CsvResponse::fromString()`

* `CsvResponse::fromFile()`

* `CsvResponse::fromArray()` (or `fromTraversable()`)

Finally, this could be interesting either as part of Diactoros, or as a stand-alone package that uses PSR-17 to create the initial stream and response instances. If you want to push it here, we can definitely maintain it, though.

Hey @weierophinney, I've been having a think about this one. As I see it at the moment, keeping it part of Diactoros, to me, makes the most sense. I'll aim to get it implemented this week.

Matthew

settermjd commented 5 years ago

I've made a start on working the three methods, however, I think they're going to take a bit of time to create and test thoroughly enough to feel comfortable with.

weierophinney commented 5 years ago

I think they're going to take a bit of time to create and test thoroughly enough to feel comfortable with.

No worries - we're pretty busy finalizing the Laminas migration tooling as it is. :smile:

settermjd commented 5 years ago

I think they're going to take a bit of time to create and test thoroughly enough to feel comfortable with.

No worries - we're pretty busy finalizing the Laminas migration tooling as it is. smile

I can only imagine.

michalbundyra commented 4 years ago

@settermjd Would you be able to resolve conflicts on this PR, please? Not sure what exactly happened but github is not showing proper diff on custom-responses.md file. Thanks !

EDIT: Is it not WIP anymore, right? If so, please update PR subject.

settermjd commented 4 years ago

@michalbundyra, sorry for disappearing. I'm working on it this week until it's completed.

Xerkus commented 4 years ago

I would like to reiterate here for the record my objections I voiced before in chat for approach introducing specialized response types as opposed to straight up factories.

Functionality introduced here is a factory method pattern. I think what it tries to achieve here violates single responsibility principle in that psr message represents complex http response structure and Csv and Download responses here are building response. It does not introduce any new or changed behavior and as such does not call for an inheritance.

Look at CsvResponse constructor from PR:

    public function __construct($text, int $status = 200, string $filename = '', array $headers = [])
    {
        if (is_string($filename) && $filename !== '') {
            $headers = $this->prepareDownloadHeaders($filename, $headers);
        }
        parent::__construct(
            $this->createBody($text),
            $status,
            $this->injectContentType('text/csv; charset=utf-8', $headers)
        );
    }

Same code as factory method using no inheritance:

class DownloadResponseFactory
{
    public function prepareResponse($text, int $status = 200, string $filename = '', array $headers = []) : Response
    {
        if (is_string($filename) && $filename !== '') {
            $headers = $this->prepareDownloadHeaders($filename, $headers);
        }
        $headers = $this->injectContentType('text/csv; charset=utf-8', $headers);
        return new Responsex(
            $this->createBody($text),
            $status,
            $headers
        );
    }
}

Same functionality, same code, single purpose. No inherited behavior adding inherited complexity cost. Simpler. Makes it easier to test with high confidence. High confidence in general means better long term maintainability.

As added bonus, if extracted as a factory/builder it gives extra flexibility:

Convenience factory to create from file:

class DownloadResponseFactory
{
           public function prepareFromFile(string $file, int $status = 200, array $headers = []) : Response;
}

May be partial downloads?

class DownloadResponseFactory
{
           public function preparePartialFromFile(int $rangeStart,  int $rangeEnd, string $file,  ...) : Response;
}

Create response object from any provider:

class CsvResponseFactory
{
           public function __construct(PsrResponseFactory $responseFactory, PsrStreamFactory $streamFactory);
}

Now, from library/framework standpoint: introduction of subclass entices users to make rigged design decisions. if ($response instanceof CsvResponse) { doLogicHere(); } appear as a valid and very appealing choice but it is a trap with implicit issues.

What said above comes from the point of view of someone with heavy focus on long term maintainability and defensive programming practices. It is not an absolute and there are other aspects to be considered.

This is my opinion as a developer, not as a ZF Community Review Team member

settermjd commented 4 years ago

Hey @Xerkus, to be honest, in the chat, I didn't fully appreciate what you were trying to say. However, after reading through such detailed feedback, what you're saying makes a lot of sense to me and, to be honest, I'm thinking that your approach is the correct one. What most stands out for me is the preparePartialFromFile. That looks like a great idea.

That said, the PR is still a WIP. The CSVResponse class doesn't reflect the presence of the DownloadResponse class. I should have removed it so that it didn't cause anyone any confusion.

weierophinney commented 4 years ago

This repository has been closed and moved to laminas/laminas-diactoros; a new issue has been opened at https://github.com/laminas/laminas-diactoros/issues/6.

weierophinney commented 4 years ago

This repository has been moved to laminas/laminas-diactoros. If you feel that this patch is still relevant, please re-open against that repository, and reference this issue. To re-open, we suggest the following workflow: