Front to Back 03

Approaching the filesystem.

What is this?

The Front to Back WordPress plugin is meant to be used by WordPress developers to quickly create small sites with a flow similar to the one offered by the [Perch CMS](https://grabaperch.com/ “Perch - The really little content management system).
I’ve gone into details in earlier posts (here and her e) and will not repeat myself.

Scenarios

One of the main tasks the plugin will fulfill is creating a template file each time the user adds a page in the administration area; the plugin should also take care of moving (“renaming” in *nix terms) said template files should the user edit the template name.
In BDD terms this could mean scenarios like the ones below:

  1. Page creation
    • Given the user can create pages and is the admin area
    • When the user creates the “About” page
    • Then the plugin should create the about.php template in the templates folder
    • And set its contents to those of the master.php template
  2. Post name change
    • Given the user can edit page post names and is editing the “About” page
    • And the “About” page has a post_name of aboout
    • When the user changes the post name to about-us
    • And saves the page
    • Then the plugin should move the about.php file to about-us.php file
    • And the contents should be the same
  3. Page deletion
    • Given the user can delete pages
    • When the user deletes the “About” page
    • Then the plugin should move the about.php template to the /deleted templates sub-folder
  4. Page recreation
    • Given the user can create pages
    • And the user has previously deleted the “About” page
    • And the about.php template is in the /deleted templates sub-folder
    • When the user creates the “About” page again
    • Then the plugin should move the about.php template back into the templates folder

The elephant in the room

Filesystem? WordPress? Oh no. I shy away from WordPress filesystem most of the times as most of the times the solution implying the reading and writing of files is just a shortcut not to use the database.
In this specific case the whole plugin revolves around the possibility for the developer to edit real template files that are kept in sync with the pages created/managed in the WordPress admin area and so there is no way around it.
In technical terms the WordPress Filesystem API is a marvelous abstraction taking away from the developer the hassle of understanding the underlying OS file system, access rights and access methods.
Any WordPress user uses it anytime a plugin or theme is installed or updated from within WordPress and thinking about the all the code I don’t have to write and test thanks to it puts things in another perspective.
This means that a PHP simple operation like writing a string to file could go from this

file_put_contents('foo.php','baz bar');

to something a little longer (see Otto on WordPress Filesystem API tutorial).
Now add TDD to it and have fun.

Wrapping the WordPress filesystem

In a way similar to what many packages do I’ve wrapped the filesystem operations in a class I will use throughout the plugin and I will be able to mock.
So that class would define an exists method that will be in charge of asserting that a file or folder exists.
This method would usually be a wrapper around a call to the file_exists function: in this context the wrapped function will be the WP_Filesystem_Base::exists($file) method.
The wrapping flow will be this:

  • tad\FrontToBack\Templates\Filesystem class wraps \WP_Filesystem_Base
  • \WP_Filesystem_Base wraps whatever the access method to the filesystem is

The operations per se are very much the same a direct access to the file system would allow but the crucial point is that the WordPress filesystem could require the user for credentials to access the filesystem.
Since this credential requirement could stop any file reading or writing operations on its tracks that credentials requirements and possibly access denial requires gracious handling: tests away. Tests scaffolding is the usual drill

wpcept generate:wpunit functional "\tad\FrontToBack\Templates\Filesystem"

and the Filesystem class itself is just an empty shell for the time being.
I will use function-mocker to handle object mocking, function replacement and assertions; that’s aliased to Test in the test class.
A first test to make sure the class is instantiatable and that the WordPress filesystem class can be safely injected

<?php
namespace tad\FrontToBack\Templates;

use tad\FunctionMocker\FunctionMocker as Test;

class FilesystemTest extends \WP_UnitTestCase {

    protected $backupGlobals = false;

    public function setUp() {
        // before
        parent::setUp();

        // your set up methods here
        Test::setUp();
    }

    public function tearDown() {
        // your tear down methods here
        Test::tearDown();
        // then
        parent::tearDown();
    }

    /**
     * @test
     * it should be instantiatable
     */
    public function it_should_be_instantiatable() {
        Test::assertInstanceOf('tad\FrontToBack\Templates\Filesystem', new Filesystem());
    }

    /**
     * @test
     * it should allow setting the template root folder
     */
    public function it_should_allow_setting_the_template_root_folder() {
        $sut = new Filesystem( __DIR__ );

        Test::assertEquals( trailingslashit( __DIR__ ), $sut->get_templates_root_folder() );
    }

    /**
     * @test
     * it should allow for WP filesystem injection
     */
    public function it_should_allow_for_wp_filesystem_injection() {
        $wpfs = Test::replace( '\WP_Filesystem_Base' )->get();

        $sut = new Filesystem( __DIR__, $wpfs );

        Test::assertSame( $wpfs, $sut->get_wpfs() );
    }

}

This test methods will cover the base of the class instantiation and dependency injection but the tricky part will be making sure the class constructor method will return a ready to run object where “ready to run” means that credentials have been checked and filesystem access has been granted.
A first gotcha here is that the files defining the WP_Filesystem_Base class and its extensions are not loaded by default and thus the class constructor will have to require the files; the current constructor method looks like this

public function __construct( $templates_root_folder = null, \WP_Filesystem_Base $wpfs = null ) {
    require_once ABSPATH . '/wp-admin/includes/class-wp-filesystem-base.php';
    require_once ABSPATH . '/wp-admin/includes/file.php';
    $this->templates_root_folder = trailingslashit( $templates_root_folder );
    $this->wpfs                  = $wpfs;
}  

Next step is making sure the class, not provided with an instance of the \WP_Filesystem_Base class, will go and properly instance one by itself

/**
 * @test
 * it should instance WP Filesystem Base class if not provided
 */
public function it_should_instance_wp_filesystem_base_class_if_not_provided() {
    $sut = new Filesystem( __DIR__ );

    Test::assertInstanceOf( '\WP_Filesystem_Base', $sut->get_wpfs() );
} 

this test will fail and I’ve modified the constructor to make it pass

public function __construct( $templates_root_folder = null, \WP_Filesystem_Base $wpfs = null ) {
    require_once ABSPATH . '/wp-admin/includes/class-wp-filesystem-base.php';
    require_once ABSPATH . '/wp-admin/includes/file.php';

    $this->templates_root_folder = trailingslashit( $templates_root_folder );
    if ( empty( $wpfs ) ) {
        $this->initialize_wp_filesystem();
    } else {
        $this->wpfs = $wpfs;
    }    
}

public function initialize_wp_filesystem( $templates_root_folder = null ) {
    if ( empty( $this->wpfs ) ) {
        $templates_root_folder = $templates_root_folder ? $templates_root_folder : $this->templates_root_folder;
        $url = admin_url( 'admin.php?page=ftb_options' );
        if ( false === ( $creds = request_filesystem_credentials( $url, 'direct', false, $templates_root_folder, null ) ) ) {
            return false;
        }

        if ( ! WP_Filesystem( $creds ) ) {
            request_filesystem_credentials( $url, 'direct', true, $templates_root_folder, null );

            return false;
        }
        global $wp_filesystem;
        $this->wpfs = $wp_filesystem;

        return true;
    }

    return ! empty( $this->wpfs );
}

Now this will make the test pass but it’s not handling the case where credentials will not work at all.

Next

I will delve deeper in the WordPress Filesystem API the only way I can: using tests.