Problem/Motivation
The documentation for \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface::getViaScheme()
and \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface::getViaUri()
indicates that these methods should return "a new stream wrapper object", but in reality these methods always return the same instance of a given stream wrapper. More specifically, the methods return the singleton service object that the Symfony service container has instantiated for the given stream wrapper type. Originally, I was going to add this to
๐
Stream Wrapper Manager Needs Inline Documentation Updates
Needs review
as yet another documentation update issue, but this is a case where I believe the implementation itself is flawed.
Despite being declared like services, stream wrappers do not behave like normal service objects because Drupal registers their types with PHP's stream wrapper API. When interacting with a custom scheme/protocol like public://
or private://
using PHP API methods like fopen()
, stat()
, or dirname()
, PHPโnot Symfonyโis responsible for the lifetime of each stream wrapper. For example, if you perform an fopen()
call on public://example.png
, PHP will make a new instance of \Drupal\Core\StreamWrapper\PublicStream()
, invoke its constructor, if one existed, and call setUri()
on the instance. This is the "normal" way that each stream wrapper expects to be used.
Meanwhile, it looks like when
#2382859: Remove file_stream_wrapper_get_*() and file_get_stream_wrappers() โ
was implemented in Drupal 8.0, the documentation was modified to reflect that a new instance gets returned (just like would happen with a stream wrapper instantiated by PHP) but the code for this from
#2028109: Convert hook_stream_wrappers() to tagged services. โ
had not implemented logic to match this. Instead, the same object that exists in the service container gets returned each time.
This is bad because code that relies on these methods might not realize that it's getting the same object each time, and that calls to setUri()
, stream_open()
, and stream_close()
could be bashing the state of a different stream opened elsewhere, at best leading to file handles being left open after they are no longer needed, and at worst leading to the same handle being closed twice. I searched through Core but outside of tests it appears that only \Drupal\Core\File\FileSystem
retrieves stream wrappers in this way. I am not sure how prevalent this use of stream wrappers is in contrib.
Steps to reproduce
In an environment where assertions are enabled, run the following code in a test or evaluate it with Devel PHP:
$manager = \Drupal::service('stream_wrapper_manager');
assert($manager instanceof StreamWrapperManagerInterface);
$wrapper1 = $manager->getViaScheme('public');
$wrapper2 = $manager->getViaScheme('public');
$wrapper1->setUri('public://test.png');
// This line emits an assertion failure.
assert($wrapper2->getUri() !== 'public://test.png');
Indeed, when I debug the code, I can see that both of the stream wrappers are the same object:
Proposed resolution
The \Drupal\Core\StreamWrapper\StreamWrapperManager::getWrapper()
method, which both of the public methods depend on, should be modified to return a new instance of the stream wrapper each time, rather than returning the instance created by the service container.
Merge request link
Remaining tasks
User interface changes
API changes
The Stream Wrapper Manager will no longer return the same instance when given the same protocol scheme in calls to getViaScheme()
or getViaUri()
.
Data model changes
Release notes snippet