-
Notifications
You must be signed in to change notification settings - Fork 152
Add DownloadResponse Class #361
base: develop
Are you sure you want to change the base?
Add DownloadResponse Class #361
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @settermjd !
I am not maintainer so I can't approve this PR, but please see my comments. As I said I don't know if this feature is gonna be accepted.
I would consider passing the array to CsvResponse and build the body inside from the array. Similar we have with JSON response - we are passing array and json is build inside.
* | ||
* Allows creating a CSV response by passing a string to the constructor; | ||
* by default, sets a status code of 200 and sets the Content-Type header to | ||
* text/csv. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wouldn't be better to accept also/only array with data and process it here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Honestly, hadn't considered that. Thanks.
test/Response/CsvResponseTest.php
Outdated
|
||
} | ||
|
||
public function invalidHeadersWhenDownloading() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can have RTH here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, what's RTH?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Return Type Hint declaration:
public function invalidHeadersWhenDownloading() : array
^^^^^^^^
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My bad.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 providedDownloadResponseTrait
. 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()
(orfromTraversable()
)
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.
Sorry that I let this slip. I'll get it finalised this week, all being well. |
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 |
cdc3fd9
to
1bd3919
Compare
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. |
No worries - we're pretty busy finalizing the Laminas migration tooling as it is. 😄 |
I can only imagine. |
@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 EDIT: Is it not WIP anymore, right? If so, please update PR subject. |
This class is almost identical to TextResponse, except that it sets the Content-Type header to text/csv.
After doing a bit of searching, I didn't find anything that would help with sending a downloaded response, instead of the default, which is to send a text response. So I created this one, which sends six headers that ensure that the response sent will be interpreted by the client as a downloadable file.
This change update the CsvResponse to be able to either send a plain CSV text response, or to send the response as a downloadable file instead.
In the style of the existing Response class documentation, this change adds a section covering the new CsvResponse class.
@michalbundyra, sorry for disappearing. I'm working on it this week until it's completed. |
The intent here is to kick of the refactor of the trait to a class and to follow it up progressively with further commits that complete the refactor.
Over the course of several projects I've found this package to be excellent for mocking the filesystem, which is required for the DownloadResponse class.
This is following on from what mwop suggested in that the body of the response can be set from either a string or a file. It's making use of a Stream object and the body is instantiated similarly to XmlResponse as how that works makes a lot of sense in this case.
After thinking about this at some length, it makes sense to me (at least at this stage) to set the content-type and content-disposition headers from values supplied ($contentType and $filename values respectively) to the constructor. This, I think, will be cleaner and more maintainable.
2d6a077
to
56ddc75
Compare
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.
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 |
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 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. |
I think that I misunderstood what was being asked originally regarding working with a DownloadResponseTrait and a CsvResponse. Given that, this change is being made to create a DownloadResponse but keep the core of the functions in the trait. Not sure if this is the correct approach, but it makes sense to me at the moment.
This repository has been closed and moved to laminas/laminas-diactoros; a new issue has been opened at laminas/laminas-diactoros#6. |
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:
|
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.