Chronicles of a build - the theme framework 07

I’ve got all green with the helper classes but still can’t use them effectively. I can now spot the problem that, to me, is a real plague: I’m a serial coder.

I’ve coded too much

Having coded an helper class to handle file paths, one for namespaces and one for element names in general I now find myself cornered in not being able to intuitively use the code I’ve written, which I know it’s working from tests, due to the fact that, simply, I do not need it or do not need it that way.

Again: start from the end

Today’s lesson is an old one and very much true when it comes to Test-Driven Development. Starting from the end means working outside-in from the final code to the needed pieces. In code terms this means, in the Main class, I want to go from this piece of code

    public static function maybeInit()
{
    if (self::$controllers !== null) {

        return;
    }
    // set the root folder
    self::setRoots();
    // if there is not a /controllers folder throw an exception
    $controllersFolderPath = self::$rootFolder . '/controllers';
    if (!is_dir($controllersFolderPath)) {

        throw new \RuntimeException('There is not a controllers folder!');
    }
    // if the controllers folder does not contains controller files or the controller files are not properly named throw an exception
    $count = count(glob($controllersFolderPath . '/*Controller.php'));
    if ($count == 0) {

        throw new \RuntimeException('Either the controllers folder is empty or does not contain properly named controller files!');
    }
    $instances = 0;
    self::$controllers = array();

    foreach (glob(self::$rootFolder . '/controllers/*.php') as $controllerFile) {
        // include the source file for the controller
        include_once $controllerFile;
        // get the controller class short name
        $controllerClassShortName = preg_replace("~(.*?)([^/]*)Controller.php~um", "$2", $controllerFile);
        // if there was no change then the file is not a controller defining file
        if ($controllerClassShortName == $controllerFile or is_null($controllerClassShortName)) {

            continue;
        }
        $controllerClassName = self::$namespace . '\controllers\\' . $controllerClassShortName . 'Controller';
        // instantiate and store a new controller reference
        // try {
        self::$controllers[$controllerClassShortName] = new $controllerClassName();
        // }
        // catch(\Exception $e) {
        //  self::politelyEcho($e->getMessage());
        // }
        // increment the numbers of instantiated controllers
        $instances+= 1;
    }

    return $instances;
}

To something like

    public static function maybeInit()
{
    if (self::$controllers !== null) {

        return;
    }
    // set the root folder
    self::setRoots();
    // get a Bot instance
    $bot = new Bot(self::$rootFolder, self::$namespace);
    // get the controllers relative to the root folder
    self::$controllers = bot->getControllers();
    if (count($controllers) == 0) {

        throw new \RuntimeException('Either the controllers folder is empty or does not contain properly named controller files!');
    }

    return count($controllers);
}

This time outside-in

After creating the libs/mvc/helpers/Bot.php file that will define the class and the corresponding BotTest class I will develop the needed methods with the barest possible code.
To implement the method called in the above code, getControllers, I will need to implement the Bot class __construct method first and will set-up a test for it in the BotTest class

    public function testWillReturnInstanceForGoodArgs()
{
    $path = dirname(dirname(__FILE__)) . '/mvc_helpers/TestMain.php';
    $namespace = '\TAD\Test';
    $this->assertNotNull(new Bot($path, $namespace)); }

Here I’m using a good path and a good namespace, the one I’ve setup for Main class tests, to test for a simple construct. I always test the construct method because while it might look trivial in its actual incarnation adding conditionals and checks in it could break it.
The first implementation of the __construct method is trivial in fact

    public function __construct($path, $fullNamespace)
{
    if (!is_string($path) or !is_string($fullNamespace)) {

        throw new \InvalidArgumentException("Path and namespace must be strings", 1);
    }
    // if the path does not exist throw
    if (!file_exists($path)) {

        throw new \InvalidArgumentException("$path does not exist", 2);
    }
    // if the namespace is not in a valid format throw
    if (!preg_match("/^[\\w_\\\\]*$/um", $fullNamespace)) {

        throw new \InvalidArgumentException("$fullNamespace is not a valid namespace", 3);
    }
    $this->path = $path;
    $this->fullNamespace = $fullNamespace;
}

and will pass the test.

Make it stateful

I’ve gone the path of implementing the Bot helper class as an instance-based helper class, in place of the previous days static helpers, to reap two benefits:

  1. I will be able to easily mock it in tests
  2. I can make the instance very stateful and avoid passing the same parameters over and over

And the code snippet from the wanna-be Main class I’ve pasted above expects the class to behave like that calling the getControllers method with no parameters at all. After some thinking I will make the class filepath-aware and namespace-aware meaning that, given filepath and namespace starting points, it will know where the rest of the mVC components are both as file organization and as namespacing. All this assuming the user did set-up both right.
Stateful means I will assign, at __construct time, the following variables

protected $path = null;                         // construct parameter
protected $rootFolderPath = null;               // construct parameter
protected $controllersFolderPath = null;
protected $viewsFolderPath = null;
protected $scriptsFolderPath = null;
protected $stylesFolderPath = null;
protected $fullNamespace = null;
protected $rootNamespace = null;
protected $controllersNamespace = null;
protected $viewsNamespace = null;

And will test that those are set as expected in a “good case” in the BotTest class. To do so I must be able to get the value of the properties and will use the __get magic method to do so and implement the above properties as read-only

public function __construct($path, $fullNamespace)
{
    if (!is_string($path) or !is_string($fullNamespace)) {

        throw new \InvalidArgumentException("Path and namespace must be strings", 1);
    }
    // if the path does not exist throw
    if (!file_exists($path)) {

        throw new \InvalidArgumentException("$path does not exist", 2);
    }
    // if the namespace is not in a valid format throw
    if (!preg_match("/^[\\w_\\\\]*$/um", $fullNamespace)) {

        throw new \InvalidArgumentException("$fullNamespace is not a valid namespace", 3);
    }
    $this->path = $path;
    $this->fullNamespace = $fullNamespace;
}
public function __get($name)
{
    if (!isset($this->$name)) {

        return null;
    }

    return $this->$name;
}

To test that the properties are properly set I will test them singularly using a data provider to be able to test them independently one of the other

public function propertyDataProvider()
{

    return array(
        array(
            'path',
            dirname(dirname(__FILE__)) . '/mvc_helpers/TestMain.php'
        ) ,
        array(
            'fullNamespace',
            '\TAD\Test'
        ) ,
        array(
            'rootFolderPath',
            dirname(dirname(__FILE__)) . '/mvc_helpers'
        ) ,
        array(
            'controllersFolderPath',
            dirname(dirname(__FILE__)) . '/mvc_helpers/controllers'
        ) ,
        array(
            'viewsFolderPath',
            dirname(dirname(__FILE__)) . '/mvc_helpers/views'
        ) ,
        array(
            'scriptsFolderPath',
            dirname(dirname(__FILE__)) . '/mvc_helpers/scripts'
        ) ,
        array(
            'stylesFolderPath',
            dirname(dirname(__FILE__)) . '/mvc_helpers/styles'
        ) ,
        array(
            'rootNamespace',
            '\TAD\Test'
        ) ,
        array(
            'controllersNamespace',
            '\TAD\Test\controllers'
        ) ,
        array(
            'viewsNamespace',
            '\TAD\Test\views'
        )
    );
}
/**
 * @dataProvider propertyDataProvider
 */
public function testConstructSetsPropertiesRight($property, $expected)
{
    $this->assertEquals($expected, $this->sut->$property, "Property $property not set correctly");
}

At all green this is the code I get

    public function __construct($path, $fullNamespace)
{
    if (!is_string($path) or !is_string($fullNamespace)) {

        throw new \InvalidArgumentException("Path and namespace must be strings", 1);
    }
    // if the path does not exist throw
    if (!file_exists($path)) {

        throw new \InvalidArgumentException("$path does not exist", 2);
    }
    // if the namespace is not in a valid format throw
    if (!preg_match("/^[\\w_\\\\]*$/um", $fullNamespace)) {

        throw new \InvalidArgumentException("$fullNamespace is not a valid namespace", 3);
    }
    $this->path = $path;
    $this->fullNamespace = $fullNamespace;
    /**
     * Set the paths
     */
    $this->setFolderPaths($path);
    /**
     * Set the namespaces
     */
    $this->setNamespaces($fullNamespace);
}
protected function setFolderPaths($path)
{
    // set the path to the current folder
    $pathToCurrentDir = rtrim($path, '/');
    if (is_file($path)) {
        $pathToCurrentDir = dirname($path);
    }
    // if the current folder is not a component folder then it's considered to be the one containing the main
    $this->rootFolderPath = $pathToCurrentDir;
    // basename will something like "controllers" or "app"
    $folderName = basename($this->rootFolderPath);
    if (in_array($folderName, self::$subFolders)) {
        // set the root path
        $this->rootFolderPath = dirname($pathToCurrentDir);
    }
    // set the sub folder paths
    $this->controllersFolderPath = $this->joinPaths($this->rootFolderPath, 'controllers');
    $this->viewsFolderPath = $this->joinPaths($this->rootFolderPath, 'views');
    $this->scriptsFolderPath = $this->joinPaths($this->rootFolderPath, 'scripts');
    $this->stylesFolderPath = $this->joinPaths($this->rootFolderPath, 'styles');
}
protected function setNamespaces($fullNamespace)
{
    $this->rootNamespace = preg_replace("/^\\\\?([\\w_\\\\]*?)\\\\?(controllers|views)?$/um", "\\\\$1", $fullNamespace);
    $this->controllersNamespace = preg_replace("/^\\\\?([\\w_\\\\]*?)\\\\?(controllers|views)?$/um", "\\\\$1\\\\controllers", $fullNamespace);
    $this->viewsNamespace = preg_replace("/^\\\\?([\\w_\\\\]*?)\\\\?(controllers|views)?$/um", "\\\\$1\\\\views", $fullNamespace);
}
public function __get($name)
{
    if (!isset($this->$name)) {

        return null;
    }

    return $this->$name;
}
protected function joinPaths($path1, $path2)
{

    return rtrim($path1, '/') . '/' . ltrim($path2, '/');
}

I’m not pasting here all the test but those can be found here on GitHub.