Closed digitalkaoz closed 8 years ago
thanks @digitalkaoz !
thanks @digitalkaoz !
Yes, indeed!
Have we looked att Guzzles implementation of Multipart stream? (ref). I cant find an implementation from diactoros...
create a multipart stream in guzzle:
private function createBody(array $params, array $files)
{
$streams = [];
foreach ($params as $k => $v) {
$streams[] = ['name' => $k, 'contents' => $v];
}
foreach ($files as $k => $file) {
$streams[] = ['name' => $k, 'contents' => new LazyOpenStream($file, 'r'), 'filename' => $file];
}
StreamFactoryDiscovery::find()->createStream(new MultipartStream($streams));
}
Interesting. I do like this on my Mailgun PR:
protected function send($method, $uri, $body = null, $files = [], array $headers = [])
{
$headers['User-Agent'] = Api::SDK_USER_AGENT.'/'.Api::SDK_VERSION;
$headers['Authorization'] = 'Basic '.base64_encode(sprintf('%s:%s', Api::API_USER, $this->apiKey));
if (!empty($files)) {
$body = new MultipartStream($files);
$headers['Content-Type'] = 'multipart/form-data; boundary='.$body->getBoundary();
}
$request = new Request($method, $this->getApiUrl($uri), $headers, $body);
$response = $this->getHttpClient()->sendRequest($request);
return $this->responseHandler($response);
}
$files is an array with ['name'=>'foo', 'contents'=>'bar']
@Nyholm mh, you are overwriting the existing body in case there are any files...so you either have a stream, or a new stream with files (the old one ist lost). guzzle handles the boundary itself afaik...
LazyOpenStream could be replaced with a normal resource in my example...
we need to support something like that (sending both, files and parameters)
@Nyholm mh, you are overwriting the existing body in case there are any files...so you either have a stream, or a new stream with files (the old one ist lost). guzzle handles the boundary itself afaik...
Yes, that is correct. I know it is weird. But it makes (kind of) sense in the case of Mailgun. Either you have files or a body.
guzzle handles the boundary itself afaik...
Do you mean that I do not need this line?
$headers['Content-Type'] = 'multipart/form-data; boundary='.$body->getBoundary();
But what if I use the Curl client to send the request. Then I need to set this header, right?
so whats todo to get this merged? @Nyholm
I'll review this today or tommorrow. Thanks for the PR.
i still feel extending the simple StreamFactoryInterface would be more convenient for consumers of the interface. if you need both, you would usually have to expect two parameters and pass the same object twice... but if everybody else thinks differently, we can leave as is.
im on your side @dbu ;) all other felt different
I've gone through the PR, looks good. Added a few questions though which are yet unclear to me.
About interface extension: I kind of agree with both of you. I see the point that MultipartStreamFactory is probably not implemented without, but inheritance is kind of pointless here. However, this is probably the case when usability should win.
About interface extension: I kind of agree with both of you. I see the point that MultipartStreamFactory is probably not implemented without, but inheritance is kind of pointless here. However, this is probably the case when usability should win.
Lets make the MultipartStreamFactory implement the StreamFactory then. I just wanted to challenge that because the answer was not obvious.
Going through this PR it seems to me that only one open question remains: What do we accept in the files parameter?
I think we should at least support Streams, because files might not always be available from local paths (eg. reading file from a remote location or loading file "content" from database).
I also think that we are free to support file paths as well.
WDYT?
what does it take to open a file? fopen($path);
? maybe we want to limit this to streams for simplification? on the other hand, allowing a string is convenient as in reality, its not only fopen but also error checking and throwing a readable exception.
there are some todos
im a bite lost everybody wants something slightly different ;)
i would support both streams and paths in the files
array ?!
I proposed to support at least Streams, optionally paths.
@dbu proposes to support streams, but does not support paths. Too much hassle with error handling, and we lose the ability to pass strings to the factory. Am I right?
so $files
will be @var StreamInterface[] $files
? do we need to ensure the streams are valid files?
if we only support streams, should we have a discoverable FileStreamFactory? im definitly for Streams and files, so the typehint would be @var array $files StreamInterface[] or string[]
and nothing needs to be changed. i mean we are discussing about a docblock?!
ups, this was left lying around, sorry.
i am ok if we support opening a file. but in that case we need to have a well-documented exception if the file is not found or can't be read from. imo, the definition must be clear. if we support streams and file paths, every factory should support both, or we have little surprises between implementations that make them not interchangeable.
@digitalkaoz: note that there is https://github.com/php-fig/fig-standards/pull/759/ discussing about adding factories in the fig. more eyes looking at that proposal are always welcome.
lets try to wrap this PR up and get it merged. even if fig eventually has this, it might still take a while for it to happen.
Any updates on this? Are there any questions that needs to be resolved?
I think @dbu has been more involved in this PR, maybe he can tell us about the open questions.
the current FIG draft also does not want to open files for you, if i read https://github.com/php-fig/fig-standards/blob/master/proposed/http-factory/http-factory.md#24-streamfactoryinterface correctly. imo we should be as close to the FIG proposal as possible. there is no multipart but imo we should think how to keep the implementation as lightweight and simple. could we rely on the http StreamInterface as list of streams?
Files are not always an obvious source for a multipart stream IMHO, so I have no problem with NOT including that in the interface. We can have a helper class or something though in the message package.
@digitalkaoz Do you have time to make a last changes to this PR?
@Nyholm i would...but i really dont get it where are all your ideas heading to...i didnt thought a simple enhancement would lead to this discussion
I agree, there has been a lot of discussions for just 27 lines of code. Sorry for that. This thread as been a bit out of control. But one sure need discuss and rethink things thoroughly when introducing a new interface like this. What we should support and what we shouldn't and also considering various use cases.
I think there are not that much left open for discussion. I can do a summary later today if you want to. Or you may do the changes directly.
a summary would be nice :)
yeah sorry for the lengthy discussion. but indeed, this interface is really hard to change once people started using it, so we need to be clear what we want. and underspecified interfaces in a place like this would also be a problem.
Here is a summary of what we talked about so far.
No, because either you need a Stream or a MultiPartStream, a client would never need both. But it will be easer to use the MultipartStreamFactory if they extend each other. I suggest to introduce a third interface extending both StreamFactory and MultipartStreamFactory. Just like our existing interface MessageFactory
.
We all agree that StreamInterfaces should be valid. Guzzle supports StreamInterface, resource and string (not path). We been back and forth arguing if paths should be allowed. The issue is complex file handling and too many things that may go wrong when reading a file. What we have not mention is resources.
$resource = fopen($path, 'r');
I suggest support for StreamInterfaces and resources. That would leave out the complexity and be similar to our own StreamFactory::createStream
which supports string|resource|StreamInterface|null
public function createMultiPartStream(array $parameters = [], array $files = []);
We have not talked about parameters a lot. It is only David that stated he wants to keep $files and $parameters apart. A parameter value could be 'filename'. We have not discussed if the 'formname' should be keys to $files or a parameter itself.
I see an issue with splitting them up because they are so tightly coupled. What if you have two $files but the first one does not have any parameters. The order of those array is obviously important and Im not sure that is good design.
Guzzle have one parameter that you may build up like this:
foreach ($files as $k => $file) {
$streams[] = ['name' => $k, 'contents' => new LazyOpenStream($file, 'r'), 'filename' => $file];
}
Mark suggests a MultipartStreamBuilder. Is that something we want to investigate further in?
What about the boundary header? There must we a way to retrieve (or set) the boundary. Guzzle does this themselves like this: Ref.
I suggest that we should either have a MultipartStreamBuilder::getBoundery()
or create a MultipartStream
interface ourselves.
Multipart
and not MultiPart
.Reviewing this thread of discussions I'm not sure a MultipartFactory is the right way to go. My reasons are:
It maybe better to create a new repo with a MultipartStreamBuilder because it is easier to use a fluid API to build the streams. The builder could return an normal stream form Guzzle/Zend/whatever. See example: https://github.com/Nyholm/MultipartStreamBuilder
thanks for this summary. i think your last point is most convincing to me: a multipart stream builder rather than an interface for something that only exists in guzzle. the builder should be in a way that it can use guzzle or implemented with a plain stream and fill it with multipart.
that would also solve the question how to handle parameters.
- David wants streams and paths (but well documented)
i am actually not convinced about supporting file paths. i think stream interface and resources are what we should support, leaving file handling (and closing!) to the consumer. wrapping a string into a stream is also not hard to do for the consumer.
just saying, in case we end up sticking with the interface - and i guess its also relevant for the builder.
To sum up in this thread: Discussions on Slack resulted in #31. We ask for feedback on twitter but no real breakthrough was made.
We stopped the discussion because we did not want to define a MultipartStreamInterface (the return value of the factory). Instead we changed the way we look at multipart streams. If we consider them as normal streams that you create in a very specific way we would not need a MultipartStreamInterface.
We created a PSR-7 independent MultipartStreamBuilder to help us create Streams in this very specific way. Read about it in the docs: http://php-http.readthedocs.io/en/latest/components/multipart-stream-builder.html
I suggest we wait to create a MultipartStreamFactory until PHP-Fig has an interface for such implementation.
i suggest that we also close this pull request. discussion about standardization should happen in the FIG and until a standard is defined, the builder should cover the needs.
thank you @digitalkaoz for bringing this up, and thanks @Nyholm for the work on the multipart stream builder!
as first start for #29