Problem/Motivation

In #2102651: Port file_example module to Drupal 8 we decided that file_example was too complex, because it demonstrated both File APIs, as well as Drupal ways of working with stream wrappers.

Proposed resolution

Split the stream wrapper code from file_example into its own example module.

Remaining tasks

User interface changes

API changes

Data model changes

Comments

Mile23 created an issue. See original summary.

mile23’s picture

Status: Active » Needs review
StatusFileSize
new61.17 KB
jkopel’s picture

Status: Needs review » Needs work
Issue tags: +Seattle Global Sprint

Need to add stream wrapper example menu item to tool bar in examples.module examples_toolbar().

Testing StreamWrapperTest failed in the UI
CLI phpunit test unable to find any tests

+++ b/stream_wrapper_example/src/Controller/ExampleController.php
@@ -0,0 +1,30 @@
+ ¶
...
+ ¶

+++ b/stream_wrapper_example/stream_wrapper_example.module
@@ -0,0 +1,104 @@
+ * ¶
...
+ ¶

+++ b/stream_wrapper_example/templates/description.html.twig
@@ -0,0 +1,44 @@
+  ¶
...
+  ¶
...
+  ¶
...
+  ¶
...
+  ¶

White space errors

+++ b/stream_wrapper_example/src/Controller/ExampleController.php
@@ -0,0 +1,30 @@
+ * Contains \Drupal\stream_wrapper_example\Controller\ExampleController.
...
+class ExampleController extends ControllerBase {

+++ b/stream_wrapper_example/stream_wrapper_example.routing.yml
@@ -0,0 +1,51 @@
+    _controller: '\Drupal\stream_wrapper_example\Controller\ExampleController::description'

Controller Class name should be StreamWrapperExampleController.

jkopel’s picture

Status: Needs work » Needs review
StatusFileSize
new61.76 KB

Patch for #3

mile23’s picture

Status: Needs review » Needs work

Thanks, @jkopel

Quite a bit to do here, let's see who gets to it. :-)

The tests are still a bit funky, there are a bunch of coding standards issues, and maybe this will help the tests:

  1. +++ b/stream_wrapper_example/tests/src/Kernel/StreamWrapperTest.php
    @@ -0,0 +1,174 @@
    +  /**
    +   * @var \Drupal\Core\DependencyInjection\Container
    +   */
    +  protected $container;
    ...
    +    // Typically if we need our tested class to get information from the system,
    +    // we use dependency injection (DI) to get that information to the class. But
    +    // stream wrappers are unusual.  They are created automatically by PHP itself
    +    // when it calls one of the standard file functions, and for that reason, the
    +    // constructor functions of stream wrappers cannot be passed any arguments,
    +    // which prevents us from using the stardard DI technique we use in Drupal 8.
    +    // The alternative is to create a "global" container that makes our services
    +    // available to the class, which is what we do here.
    +    $container = new ContainerBuilder();
    +    $request_stack = $this->createSessionMock();
    +    $container->set('request_stack', $request_stack);
    +    $container->set('file_system', \Drupal::service('file_system'));
    +    $container->set('kernel', \Drupal::service('kernel'));
    +    \Drupal::setContainer($container);
    +    $this->container = $container;
    

    KernelTestBase gives us a $container which contains the test fixture container based on the fixture kernel, which we just told to enable stream_wrapper_example. Therefore, the existing $container should have done all the work.

    There might be some subtle thing I'm missing, though.

  2. +++ b/stream_wrapper_example/tests/src/Kernel/StreamWrapperTest.php
    @@ -0,0 +1,174 @@
    +  /**
    +   * Test dialtone.
    +   */
    +  public function testDialTone() {
    

    Rename to testSessionScheme(). Dialtone is too obscure.

  3. +++ b/stream_wrapper_example/tests/src/Kernel/StreamWrapperTest.php
    @@ -0,0 +1,174 @@
    +    $have_session_scheme = \Drupal::service('file_system')->validScheme('session');
    

    After all that with $container, we call \Drupal here? :-)

mile23’s picture

mile23’s picture

+++ b/stream_wrapper_example/src/StreamWrapper/MockSessionTrait.php
@@ -0,0 +1,101 @@
+trait MockSessionTrait {

Need to add a @todo saying we should move this to the tests namespace after #2605664: [Needs change record updates] Align TestDiscovery and bootstrap.php's non-permissive loading of PSR-4 namespaces for traits

mile23’s picture

@todo: Eventually we need to update Drupal\file_example\Form\FileExampleReadWriteForm::getSessionWrapper() to use this module.

Like this:

  protected function getSessionWrapper() {
    if ($this->sessionSchemeEnabled) {
      return new \Drupal\stream_wrapper_example\StreamWrapper\SessionWrapper($this->requestStack);
    }
    return FALSE;
  }
Torenware’s picture

Status: Needs work » Needs review
StatusFileSize
new61.83 KB

Rerolling the patch to start work on it again; just a merge, no other changes.

Torenware’s picture

StatusFileSize
new61.73 KB
new7.65 KB

Reintegrated the example with the file_example; fixed @Mile23 comments.

Torenware’s picture

StatusFileSize
new62.59 KB
new881 bytes

Added back the SessionWrapper wrangling code to the File Example.

mile23’s picture

Status: Needs review » Needs work

Very close now.... :-)

CS and docs stuff mostly. Run phpcs and see what else you find.

  1. +++ b/stream_wrapper_example/src/StreamWrapper/FileExampleSessionStreamWrapper.php
    @@ -0,0 +1,860 @@
    + * reading and writing the $_SESSION['stream_wrapper_example'] key as if it were a file.
    ...
    + *     See stream_wrapper_example.routing.yml for an example of this, and file.module
    
    +++ b/stream_wrapper_example/src/StreamWrapper/SessionWrapper.php
    @@ -0,0 +1,242 @@
    +   *   The content of the whole session data store, to replace all of the current data.
    
    +++ b/stream_wrapper_example/stream_wrapper_example.module
    @@ -0,0 +1,105 @@
    + *  - src/StreamWrapper/FileExampleSessionStreamWrapper.php (the stream wrapper class).
    + *  - src/PathProcessorSessions.php, which implements something called a "path processor".
    + *    A path processor is a class that lets us hook the "files" in a scheme's file system
    + *    into Drupal 8's routing system. This allows us to assign external URLs to the files
    + *    served out of the stream wrapper class.  Like a stream wrapper class, Drupal 8
    + *    implements this as a "tagged service".
    + *  - stream_wrapper_example.services.yml, which defines the services we need to do all
    + *    this magic. You should read the comments in that file to see how this fits together.
    + *
    + * We also include two PHPUnit tests. Because stream wrappers require quite a bit of code
    + * that gets called "behind our back" by PHP itself, good test coverage is essential for
    + * writing usable stream wrapper implementations. The tests should help get you started
    
    +++ b/stream_wrapper_example/stream_wrapper_example.routing.yml
    @@ -0,0 +1,51 @@
    +# service in our services file. Our path processor will do the extra steps needed
    ...
    +# In addition to the stream_wrapper_example.files route, which is actually matched by the router,
    +# we also need a route defintion to make our URLs.  This is never referenced by the
    +# routing system, but is used by our stream wrapper class to create external URLs.
    
    +++ b/stream_wrapper_example/stream_wrapper_example.services.yml
    @@ -0,0 +1,34 @@
    +# As part of our demo, we implement a simple "file system" that lets us read and write
    +# files out of the $_SESSION.  This isn't very practical, but it's a simple way to
    +# demonstrate what you can do with PHP's stream wrappers.
    +#
    +# To get a stream wrapper to work to define a stream wrapper class, we need to register
    +# that with the system.  We can either do this manually by calling up the 'stream_wrapper.manager'
    +# service, but the better way to do this is to have the system autoload it by tagging the service,
    +# as we do here.
    +#
    +# We also want to securely serve up our fake session files. We'd like to use the same nice
    +# file paths that Core uses for private files.  Since Drupal 8 no longer allows us to have
    +# "menu tails" (i.e., extra/parts/of/the/path after the default part of the path), we need
    +# to get some router superpowers. Our route (in stream_wrapper_example.routing.yml) will "gather up"
    +# the path with with a regular expression.  But we need to do a little more that that.  We
    +# also need to convince the routing system to see our weird, extra long route route. We
    +# do that using a "Path Processor". We register the path_process.sessions service with special
    +# tags to get it loaded for when the Drupal's routing system decides which path should get
    +# used.
    
    +++ b/stream_wrapper_example/templates/description.html.twig
    @@ -0,0 +1,44 @@
    +  <p>The Stream Wrapper Example module demonstrates a PHP stream wrapper implementation.
    +  A stream wrapper is a class that implements something that looks and behaves like a
    +  file system. A particular implementation of a stream wrapper is called a <em>scheme</em>.
    +  Drupal 8 supports public, private, and temporary wrapper schemes. For example, you
    +  access a file in your public uploads directory via a "public" file URI such as
    +  <code>public://images/big-logo.png

    . When you read, write, delete or move that
    + file, the public scheme's stream wrapper class
    + (\Drupal\Core\StreamWrapper\PublicStream) is invoked to do the reading,
    + writing, deletion or moving. PHP does this automatically for you, creating the wrapper
    + whenever some file operation needs to get done on a public:// file.
    +

    +
    +

    To demonstrate how to implement a stream wrapper, this example module creates a
    + session wrapper scheme. It uses your session data (created when you
    + log into Drupal) to create a nested array where the arrays represent directories,
    + and scalar values represent files. This is completely impractical, and frankly,
    + not terribly secure, so you should never enable this module on any site that's
    + open to the Internet. But without using any special libraries, our stream wrapper
    + class is able to create and delete directories, and read and write files.
    +

    +
    +

    If you want to play with session file URIs, we recommend also enabling
    + the File Example (file_example.module), which will let you do the same things with
    + the "session" scheme that you can do with public, private or temporary files.

    +
    +

    A longer description of what code is where can be found in
    + stream_wrapper_example.module. Definitely look through the code to see
    + various implementation details.

    +++ b/stream_wrapper_example/tests/src/Kernel/StreamWrapperTest.php
    @@ -0,0 +1,162 @@
    + * it loads in background automatically as soon as the stream_wrapper_example module
    ...
    + // we use dependency injection (DI) to get that information to the class. But
    + // stream wrappers are unusual. They are created automatically by PHP itself
    + // when it calls one of the standard file functions, and for that reason, the
    ...
    + // which prevents us from using the stardard DI technique we use in Drupal 8.

    Lots of 80 char wrap needed.

    Also lots of not-wrapped-out-to-80 stuff, as well, but that's a better problem to have, and our phpcs config doesn't force it (yet). :-)

  2. +++ b/stream_wrapper_example/src/StreamWrapper/FileExampleSessionStreamWrapper.php
    @@ -0,0 +1,860 @@
    +   * Implements setUri().
    ...
    +   * Implements getUri().
    ...
    +   * Implements getTarget().
    ...
    +   * Implements getDirectoryPath().
    

    If these are part of the interface, they should say {@inheritdoc} instead of implements.

  3. +++ b/stream_wrapper_example/src/StreamWrapper/FileExampleSessionStreamWrapper.php
    @@ -0,0 +1,860 @@
    +  /**
    +   * Support for rewinddir().
    +   *
    +   * @return bool
    +   *   TRUE on success.
    +   *
    +   * @see http://php.net/manual/en/streamwrapper.dir-rewinddir.php
    +   */
    +  public function dir_rewinddir() {
    +    $this->directoryPointer = 0;
    +  }
    

    Documents @return bool, but never calls return.

  4. +++ b/stream_wrapper_example/src/StreamWrapper/SessionWrapper.php
    @@ -0,0 +1,242 @@
    +   * Since we cannot deal with references to the session, write the whole
    +   *  store back.
    

    First line of comment should be one line.

  5. +++ b/stream_wrapper_example/src/StreamWrapper/SessionWrapper.php
    @@ -0,0 +1,242 @@
    +   * @param array $store.
    

    No . after $store.

  6. +++ b/stream_wrapper_example/tests/src/Kernel/StreamWrapperTest.php
    @@ -0,0 +1,162 @@
    +    // The following fails in the original implementation; the file is larger than the data.
    +    // $this->assertEquals($len, $size, "Size of file $uri should match the data written to it.");.
    

    We shouldn't have commented code. We should explain what's going on.

Torenware’s picture

Thanks @Mile23.

Yeah, lots of CS issues. Most should be easy enough to fix.

One thing related to this. Stream Wrappers extend a standard PHP interface. This interface itself violates certain aspects of the standard Drupal specs, especially camel case for method names -- the interface uses underscores. Can't change this, since it's not our interface.

What I *can* do is explain that we're using this interface, and quiet the apparent coding errors using @codingStandardsIgnoreStart and @codingStandardsIgnoreEnd. It's that, or accept that there will be unfix errors on the relevant files. You've got an opinion on this one?

mile23’s picture

The non-standard method names doesn't trigger the current phpcs config we have for Examples. So eventually we'll run up against that and fix it when the rule gets added.

Torenware’s picture

Status: Needs work » Needs review
StatusFileSize
new63.23 KB
new11.49 KB

No on that; I'm using the current rules, and the PublicMethod rule is applied, PITA though that is. So Drupal.NamingConventions.ValidFunctionName.ScopeNotCamelCaps is already in, and it bites me.

In any case, here's a version of the patch that fixes everything but that.

  • Mile23 committed 0f00a74 on 8.x-1.x authored by Torenware
    Issue #2638290 by Torenware, Mile23, jkopel: Create...
mile23’s picture

Status: Needs review » Fixed

I'm calling this done. :-)

Thanks @Torenware and @jkopel!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.