Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrap fopen for PHP 8 #174

Merged
merged 2 commits into from
May 10, 2021
Merged

Wrap fopen for PHP 8 #174

merged 2 commits into from
May 10, 2021

Conversation

Zegnat
Copy link
Collaborator

@Zegnat Zegnat commented Mar 8, 2021

fopen() has new behaviour where it will throw an error in PHP 8. Some projects have bumped into this, e.g. Drupal and the HTTP Factory tests.

This PR wraps our uses of fopen() to not leak the ValueError to library users.

Marking as WIP as I haven’t looked at tests yet. But have a look and see if these changes seem right!

@Nyholm
Copy link
Owner

Nyholm commented Mar 9, 2021

FYI: @GrahamCampbell did a similar PR on Guzzle: guzzle/psr7#375

@Zegnat
Copy link
Collaborator Author

Zegnat commented Mar 9, 2021

Looks like Guzzle has always had an abstraction for fopen() while we use it immediately. They always caught errors already, while we supress them and instead count on false being returned. I am not sure what the best way to go is.

I found a total of 4 instances of fopen() in our code, and would prefer to wrap them on a case-by-case basis as I have done in this PR, rather than abstracting the whole function. Especially as one of the four is inside Stream::create, not part of the PSR-7 specification, and is allowed to throw a ValueError. Leaves only three cases to address.

Looks like all tests pass, and I don’t really think we want to (or need to) add tests of our own beside the integration tests? Marking this as ready for review now.

@Zegnat Zegnat marked this pull request as ready for review March 9, 2021 14:48
src/Stream.php Outdated
@@ -61,6 +61,7 @@ private function __construct()
* @param string|resource|StreamInterface $body
*
* @throws \InvalidArgumentException
* @throws \ValueError
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have a consistent API across all PHP versions. The internals of fopen changing should not effect the public API of this class.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be removed, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically \fopen('php://temp', 'rw+'); could throw a ValueError. I have been thinking about how I might be able to use the Stream::createFromFile for it as a refactor. Otherwise we need to wrap it in a try {} catch {}.

I say theoretically because I would find it very counter-intuitive for PHP to throw a ValueError on values that are internal to PHP.

Copy link
Collaborator Author

@Zegnat Zegnat Mar 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can actually make it so there are no extra fopens in the exact same amount of code with something like:

@@ -69,12 +69,6 @@ class Stream implements StreamInterface
             return $body;
         }
 
-        if (\is_string($body)) {
-            $resource = \fopen('php://temp', 'rw+');
-            \fwrite($resource, $body);
-            $body = $resource;
-        }
-
         if (\is_resource($body)) {
             $new = new self();
             $new->stream = $body;
@@ -86,6 +80,13 @@ class Stream implements StreamInterface
             return $new;
         }
 
+        if (\is_string($body)) {
+            $stream = self::createFromFile('php://temp', 'rw+');
+            $stream->write($body);
+
+            return $stream;
+        }
+
         throw new \InvalidArgumentException('First argument to Stream::create() must be a string, resource or StreamInterface.');
     }

src/UploadedFile.php Outdated Show resolved Hide resolved
@GrahamCampbell
Copy link
Contributor

I think it would be worth just copying Guzzle's tryFopen function: https://github.com/guzzle/psr7/blob/d7fe0a0eabc266c3dcf2f20aa12121044ff196a4/src/Utils.php#L343-L389.

composer.json Outdated Show resolved Hide resolved
@Zegnat
Copy link
Collaborator Author

Zegnat commented Mar 23, 2021

I am still divided on taking the Guzzle approach wholesale. At that point I would rather people just go and use the Guzzle implementation. That is the strength of interoperability interfaces, after all. I don’t think we should be adding utility classes/methods.

Current middle of the road solution: creating a new static factory method on our Stream implementation that handles it. This also lets us minimise some code in other places and centralises us to only a single file that uses \fopen. I have also marked this method as @internal to steer end-users towards the actual factories.

src/Stream.php Outdated
*
* @internal Implements Psr\Http\Message\StreamFactoryInterface::createStreamFromFile for internal use, library users should default to the actual Factory object
*/
public static function createFromFile(string $filename, string $mode = 'r'): StreamInterface
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we kept this code in the factory and only surrounded the other two fopen with try/catch, wouldn't achieve the same thing and allow us to not have this method?

Copy link
Collaborator Author

@Zegnat Zegnat Mar 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This same method is used by moveTo and getStream from FileUpload. Because they too create a Stream instance from a path. Not sure wrapping all of them actually helps.

cloc src shows that this patch only increased out code from 1007 to 1014. So probably no more or less than wrapping everything by itself. And this way we have it centralised. But I don’t feel strongly either or.

CHANGELOG.md Outdated Show resolved Hide resolved
src/Stream.php Outdated Show resolved Hide resolved
@nickdnk
Copy link
Contributor

nickdnk commented May 9, 2021

Hello guys

Is something currently holding this PR back? This package is now the only one that prevents me from running/testing my app on PHP 8.

Copy link
Owner

@Nyholm Nyholm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you.

I added a try/catch to avoid adding a new public method.

@Nyholm Nyholm merged commit a4a3629 into master May 10, 2021
@Nyholm Nyholm deleted the wrap-fopen-for-php-8 branch May 10, 2021 20:15
@nickdnk
Copy link
Contributor

nickdnk commented May 10, 2021

So it turns out it was psr7-server and not this one that held me back. I updated that to 1.0.x and everything is good (semantic beta-versioning pulling pranks on me). However, 1.4.1 from this package is not on Packagist, so I was just wondering if you forgot to tag it? It's on the changelog: https://github.com/Nyholm/psr7/blob/master/CHANGELOG.md - but not here: https://github.com/Nyholm/psr7/releases

@GrahamCampbell
Copy link
Contributor

Not forgotten. Intentionally not released yet. Pending #179 and possibly some other fixes before the release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants