StreamWrapperManager Returns Singleton Instances of Stream Wrappers, Not New Instances

Created on 17 January 2024, 11 months ago
Updated 28 May 2024, 7 months ago

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

๐Ÿ› Bug report
Status

Active

Version

11.0 ๐Ÿ”ฅ

Component
Baseย  โ†’

Last updated 1 day ago

Created by

๐Ÿ‡บ๐Ÿ‡ธUnited States GuyPaddock

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @GuyPaddock
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States GuyPaddock
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States GuyPaddock
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States lhridley

    I disagree with this for the following reasons:

    1. When a stream wrapper is registered with the Stream Wrapper Manager service, that stream wrapper is associated with a particular scheme (i.e., public://, private:// etc.
    2. . Since you can't have two stream wrapper services associated with the same scheme, it only make sense to return a single instance.. This is not unlike other stream wrapper services that are present outside of Drupal, namely:

  • Currently the base file location is stored as part of the stream wrapper object. Changing the underlying stream wrapper associated with a scheme will break Drupal, as the managed files may no longer resolve to a valid file.
  • Since Drupal Core's managed file system assumes that entries in the file_managed table are valid file resources, changing the registered stream wrapper runs the risk of also changing the physical file location pointer. Without planning a physical file migration changing the physical file location will break Drupal, and return WSOD pages when a file entry can no longer be found.
  • If anything is wrong, it is more likely to be the documentation.

Production build 0.71.5 2024