Front to Back 08

First tests on the template to meta field conversion road.

Starting from the end

I’ve outlined in the earlier post what the code flow should be to go from a template to Custom Meta Boxes 2 managed meta fields being shown to the user in the page administration screen.
This process is a dynamic one to be carried out on each load of the page edit screen and as such the usual way to add meta boxes outlined in the wiki cannot be used as it is for the simple reason that meta boxes will not be hardcoded in a file but parsed from the template.
With this in mind I will start creating the class responsible for the meta boxes creation (MetaBoxes\Page).
In the plugin init.php file I’m registering the class in the dependency injection container and kickstarting it after plugins have loaded:

<?php
use tad\FrontToBack\Credentials\NonStoringCredentials;
use tad\FrontToBack\Fields\FieldsUpdater;
use tad\FrontToBack\MetaBoxes\Page;
use tad\FrontToBack\OptionsPage;
use tad\FrontToBack\Templates\Creator;
use tad\FrontToBack\Templates\Filesystem;
use tad\FrontToBack\Templates\MasterChecker;
use tad\FrontToBack\Templates\TemplateScanner;

require_once __DIR__ . '/src/functions/commons.php';

$plugin = ftb();

/**
 *  Variables
 */
$plugin->set( 'path', __DIR__ );
$plugin->set( 'url', plugins_url( '', __FILE__ ) );

$templates_extension = 'php';
$plugin->set( 'templates/extension', $templates_extension );
$plugin->set( 'templates/master-template-name', "master.{$templates_extension}" );

/**
 * Initializers
 */
$plugin->set( 'templates/default-folder', function () {
    return WP_CONTENT_DIR . '/ftb-templates';
} );

$plugin->set( 'options-page', function () {
    return new OptionsPage();
} );

$plugin->set( 'credentials-store', function () {
    return new NonStoringCredentials();
} );

$plugin->set( 'templates-filesystem', function () {
    $templates_folder = ftb_get_option( 'templates_folder' );
    $templates_folder = $templates_folder ?: ftb()->get( 'templates/default-folder' );

    return new Filesystem( $templates_folder, null, ftb()->get( 'credentials-store' ) );
} );

$plugin->set( 'master-template-checker', function () {
    return new MasterChecker( ftb()->get( 'templates-filesystem' ) );
} );

$plugin->set( 'templates-creator', function () {
    return new Creator( ftb()->get( 'templates-filesystem' ) );
} );

$plugin->set( 'page-meta-boxes', function () {
    return new Page();
} );

/**
 * Kickstart
 */
add_action( 'plugins_loaded', function () use ( $plugin ) {

    /** @var OptionsPage $optionsPage */
    $optionsPage = $plugin->get( 'options-page' );
    $optionsPage->hooks();

    /** @var MasterChecker $masterTemplateChecker */
    $masterTemplateChecker = $plugin->get( 'master-template-checker' );
    $masterTemplateChecker->hooks();

    /** @var Creator $templatesCreator */
    $templatesCreator = $plugin->get( 'templates-creator' );
    $templatesCreator->hooks();

    /** @var Page $metaBoxes */
    $pageMetaBoxes = $plugin->get( 'page-meta-boxes' );
    $pageMetaBoxes->hooks();
} );

And the class hooks method will take care of adding meta boxes for the page on the CMB2 hook

<?php

namespace tad\FrontToBack\MetaBoxes;


class Page {

    /**
     * Page constructor.
     */
    public function __construct() {
    }

    public function hooks() {
        add_action( 'cmb2_admin_init', array( $this, 'add_page_meta_boxes' ) );
    }

    public function add_page_meta_boxes() {

    }
}

Meta fields information

To create the page meta boxes and be able to store the page meta fields I will have to read which ones the page is supposed to show the user: this information is in the template. Copying last post example:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Page template</title>
</head>
<body>
   <h3>Here is an editable string</h3>
   <p>
       <ftb-text id='user_string' title="User string" />
   </p>
</body>
</html>

The meta fields information the template specifies is that the user should be presented with a text type meta field titled User string; in CMB2 terms this is the code that will do the job

$cmb = new_cmb2_box( array(
        'id'            => 'ftb_sample-page_fields_metabox',
        'title'         => __( 'Fields', 'ftb' ),
        'object_types'  => array( 'page', ),
        'show_on' => array( 'key' => 'id', 'value' => 9 ),
        'context'       => 'normal',
        'priority'      => 'high',
        'show_names'    => true, // Show field names on the left
) );

$cmb->add_field( array(
        'name'       => 'User string',
        'desc'       => '',
        'id'         => 'user_string',
        'type'       => 'text',
) );

Page meta fields

This code needs to be dynamically created in the Page::add_page_meta_boxes method though so down to some TDD flow.

Page template

To cover this first use case I came up with the following test case (again using function-mocker to mock and assert)

<?php
namespace tad\FrontToBack\MetaBoxes;

use tad\FunctionMocker\FunctionMocker as Test;

class PageTest extends \WP_UnitTestCase {

    protected $backupGlobals = false;

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

        // your set up methods here
    }

    public function tearDown() {
        // your tear down methods here

        // then
        parent::tearDown();
    }

    /**
     * @test
     * it should not call new_cmb2_box if template not exists
     */
    public function it_should_not_call_new_cmb_2_box_if_template_not_exists() {
        $new_cmb2_metabox = Test::replace( 'new_cmb2_box' );
        $pageTemplate     = Test::replace( '\\tad\\FrontToBack\\Templates\\TemplateInterface' )
                                ->method( 'exists', false )
                                ->get();

        $sut = new Page( $pageTemplate );
        $sut->add_page_meta_boxes();

        $new_cmb2_metabox->wasNotCalled();
    }

    /**
     * @test
     * it should not call new_cmb2_box if template has no meta fields
     */
    public function it_should_not_call_new_cmb_2_box_if_template_has_no_meta_fields() {
        $new_cmb2_metabox = Test::replace( 'new_cmb2_box' );
        $pageTemplate     = Test::replace( '\\tad\\FrontToBack\\Templates\\TemplateInterface' )
                                ->method( 'exists', true )
                                ->method( 'has_fields', false )
                                ->get();

        $sut = new Page( $pageTemplate );
        $sut->add_page_meta_boxes();

        $new_cmb2_metabox->wasNotCalled();
    }

    /**
     * @test
     * it should call new_cmb2_box id template has meta fields
     */
    public function it_should_call_new_cmb_2_box_id_template_has_meta_fields() {
        $new_cmb2_metabox = Test::replace( 'new_cmb2_box' );
        $pageTemplate     = Test::replace( '\\tad\\FrontToBack\\Templates\\TemplateInterface' )
                                ->method( 'exists', true )
                                ->method( 'has_fields', true )
                                ->method( 'get_fields', array() )
                                ->get();
        $_GET['id']       = 2;
        $sut              = new Page( $pageTemplate );
        $sut->add_page_meta_boxes();

        $new_cmb2_metabox->wasCalledOnce();
    }

    /**
     * @test
     * it should not call new_cmb2_box if id not set
     */
    public function it_should_not_call_new_cmb_2_box_if_id_not_set() {
        $expected         = [
            'id'           => 'ftb_foo-bar_fields_metabox', 'title' => __( 'Fields', 'ftb' ),
            'object_types' => array( 'page', ), 'show_on' => array( 'key' => 'id', 'value' => 9 ),
            'context'      => 'normal', 'priority' => 'high', 'show_names' => true, // Show field names on the left
        ];
        $_GET['id']       = null;
        $new_cmb2_metabox = Test::replace( 'new_cmb2_box' );
        $pageTemplate     = Test::replace( '\\tad\\FrontToBack\\Templates\\TemplateInterface' )
                                ->method( 'exists', true )
                                ->method( 'has_fields', true )
                                ->get();

        $sut = new Page( $pageTemplate );
        $sut->add_page_meta_boxes();

        $new_cmb2_metabox->wasNotCalled();
    }

    /**
     * @test
     * it should call new_cmb2_box with page based id
     */
    public function it_should_call_new_cmb_2_box_with_page_based_id() {
        $expected         = [
            'id'           => 'ftb_foo-bar_fields_metabox', 'title' => __( 'Fields', 'ftb' ),
            'object_types' => array( 'page', ), 'show_on' => array( 'key' => 'id', 'value' => 3 ),
            'context'      => 'normal', 'priority' => 'high', 'show_names' => true, // Show field names on the left
        ];
        $_GET['id']       = 3;
        $new_cmb2_metabox = Test::replace( 'new_cmb2_box' );
        $pageTemplate     = Test::replace( '\\tad\\FrontToBack\\Templates\\TemplateInterface' )
                                ->method( 'exists', true )
                                ->method( 'has_fields', true )
                                ->method( 'get_name', 'foo-bar' )
                                ->method( 'get_fields', array() )
                                ->get();

        $sut = new Page( $pageTemplate );
        $sut->add_page_meta_boxes();

        $new_cmb2_metabox->wasCalledWithOnce( [ $expected ] );
    }

    /**
     * @test
     * it should add text type field to meta box
     */
    public function it_should_add_text_type_field_to_meta_box() {
        $_GET['id']   = 3;
        $fields       = [ new Field( 'text', 'field_id', 'Field name' ) ];
        $pageTemplate = Test::replace( '\\tad\\FrontToBack\\Templates\\TemplateInterface' )
                            ->method( 'exists', true )
                            ->method( 'has_fields', true )
                            ->method( 'get_name', 'foo-bar' )
                            ->method( 'get_fields', $fields )
                            ->get();
        $expected     = [
            'name' => 'Field name', 'desc' => '', 'id' => 'field_id', 'type' => 'text',
        ];
        $mb           = Test::replace( 'CMB2' )->method( 'add_field' )->get();
        Test::replace( 'new_cmb2_box', $mb );
        $sut = new Page( $pageTemplate );

        $sut->add_page_meta_boxes();

        $mb->wasCalledWithOnce( [ $expected ], 'add_field' );
    }
}

The need to inject and control the inputs of the class exposed missing accessory classes that I’ve stubbed out to the point of bare functions.
This is typical of the TDD flow and it’s the process outlined in the book “Growing object-oriented software guided by tests” and a flow I’ve getting into more and more.
The current class version ends up being, at this point of the development, the one below:

<?php

namespace tad\FrontToBack\MetaBoxes;


use tad\FrontToBack\Templates\TemplateFactory;
use tad\FrontToBack\Templates\TemplateInterface;

class Page {

    /**
     * @var TemplateInterface|void
     */
    protected $template;

    /**
     * Page constructor.
     *
     * @param TemplateInterface $template
     */
    public function __construct( TemplateInterface $template = null ) {
        $this->template = empty( $template ) ? TemplateFactory::make() : $template;
    }

    public function hooks() {
        add_action( 'cmb2_admin_init', array( $this, 'add_page_meta_boxes' ) );
    }

    public function add_page_meta_boxes() {
        if ( ! ( $this->template->exists() && $this->template->has_fields() ) || empty( $_GET['id'] ) ) {
            return;
        }
        $id  = "ftb_{$this->template->get_name()}_fields_metabox";
        $cmb = new_cmb2_box( array(
            'id'         => $id, 'title' => __( 'Fields', 'ftb' ), 'object_types' => array( 'page', ),
            'show_on'    => array( 'key' => 'id', 'value' => $_GET['id'] ), 'context' => 'normal', 'priority' => 'high',
            'show_names' => true, // Show field names on the left
        ) );

        {
            /** @var FieldInterface $field */
            foreach ( $this->template->get_fields() as $field ) {
                $args = array(
                    'name' => $field->get_name(), 'desc' => $field->get_description(), 'id' => $field->get_id(),
                    'type' => $field->get_type(),
                );
                $cmb->add_field( $args );
            }
        }
    }
}

See full code on GitHub.

Next

I will delve in the development of the accessory classes and get to a point where I will be able to properly process the first functional template.