Часть 3. Bun Framework – конфигурация приложения

2014-06-01 в 04:51 Bun Framework PHP PHPUnit

Продолжаем серию статей по разработке Bun Framework. После того, как мы подготовили рабочий "скелет" приложения нашего фреймворка, переходим к неотъемлемой части всех веб-приложений – конфигурации. Ранее я уже упоминал, что модуль Bun\Core\Config войдет в ядро фреймворка, и кроме того в этой статье я затрону базовые зависимости модуля конфигураций: абстракция работы с файловой системой или типизированные исключения. И на последок в данной статье для проверки работоспособности написанного модуля я покажу простой пример организации unit-тестов написанного компонента фреймворка.

Bun Framework config


Для начала, в написанный в предыдущей статье класс Bun\Core\Application мы добавляем новый метод getConfig():


/**
* @return ApplicationConfig
*/
public function getConfig()
{
    if($this->config === null) {
        $this->config = new ApplicationConfig($this);
    }

    return $this->config;
}

Теперь непросредственно переходим к созданию модуля конфигурации. Идея очень простая:

  • Основные настройки разделены по пространствам имен, настройки представляются в виде php-классов (параметры храняться в виде массива в свойсве класса $config). Доступ к настройкам осуществляеться через метод get(), c использование dot-нотации. Т.е. запрос вида $config->get('ns.param') к классу, имеющему настройки вида array('ns' => array('param' => 'value')) – вернет нам значение "value".
    В каждом модулей фреймворка и в каждом приложении предопределяется подпространство имен "Config", в котором и определяются соответсвующие классы
  • Также существуют файлы параметров, в формате json, которые хранятся в одной директории с классами настроек. В классах настроек можно использовать вместо скалярных значений ссылки на значения из параметров. Файлы параметров будут разделяться по окружению (dev, prod и т.д) а также будет один базовый файл: config.json
    • При загрузке настройки переопределяют друг друга:
    • Сначала грузяться настройки классов внутри фреймворка
    • Затем настройки приложений внутри src (кроме текущего запущенного прилоежния, если такие есть)
    • Затем подгружаются настройки уже активного запущенного приложения
    • В конце в таком же порядке подгружаются файлы настроечных параметров и параметры подставляются в конфигурацию
    • Таким образом, если в конфигурации фрейворка будет задан параметр "db.driver": "mysqli", то указанный в настройках приложения такой-же параметр переопределить настройки фреймворка при инициализации конфига
  • Все php-файлы внутри директории Config в модулях фреймворка и приложений будут считываться на предмет наличия конфигурационных классов, а для json-файлов параметров считываются только файлы config.json и config_env.json, где "env" – текущее окружение (prod, dev и т.д.).


Такая немного замудреная схема позволяет добиться довольно-таки гибкого управления настройками, в целях быстродействия, возможно придется предусмотреть механизм кэширования инициализированных настроек в продакшен-окружении.

Теперь определяем, как мы все это будем кодировать:

  • Выделим пространство имен Exception внутри модуля Core, для создания типизированных исключений для обработки ошибок при работе компонента Config
  • Для чтения директорий и файлов параметров придется создать небольшую абстракцию для удобной работы с файловой системой. Первоначально это будут классы Bun\Core\FileSystem\File и Bun\Core\FileSystem\Dir.
  • Создадим абстактный класс, от которого будут наследоваться классы кофигураций
  • Реализуем класс, который будет осуществлять загурзку всех конфигураций и компоновку их в один массив, а также организует доступ к ним с помощью описанного выше метода.
  • Создадим несколько тестовых настроек и параметров и напишем небольшой тест-кейс для тестирования работоспособности компонента

Поехали!


Исключения — Bun\Core\Exception


Я выделил пространство имен и создал три типа исключений: BunException — базовый тип, от которого будем наследовать все остальные исключения (это поможет нам удобно отлавливать нужные исключения); FileSystemException — так как будем писать компоненты для работы с ФС, то понадобятся и исключения; и ConfigException — будем вываливать, когда что-то идет не так при загрузке конфигураций.

В исключениях нет особо никакого кода пока, просто болванка:


<?php
namespace Bun\Core\Exception;

/**
 * Class BunException
 *
 * @package Bun\Core\Exception
 */
class BunException extends \Exception
{

}

<?php
namespace Bun\Core\Exception;

/**
 * Class ConfigException
 *
 * @package Bun\Core\Exception
 */
class ConfigException extends BunException
{

}

//...

Bun\Core\FileSystem - немного абстрагируемся от файловой системы


При работе со встроенными функциями php для работы с файлами и директориями приходится довольно много кода писать для обработки возможных ошибок и предупреждений, да и вообще иногда лишний код для тривиальных операций приходится копипастить, поэтому очень часто разработчики используют собственные обертки над этими функциями в виде классов.

1. Bun\Core\FileSystem\AbstractFileSystemNode

Звучит страшно – но это всего лишь базовый абстрактный класс для будущих классов директории и файла. Файл и директория имеют много общего, поэтому дабы не дублировать код – используем наследование. Сюда можно вынести такие свойства ноды файловой системы, как полный путь, директория, имя без директории, время создания, изменения и доступа объекта и тому подобное, весь код писать не буду:


<?php
namespace Bun\Core\FileSystem;

/**
 * Class AbstractFileSystemNode
 *
 * @package Bun\Core\FileSystem
 */
abstract class AbstractFileSystemNode
{
    /** @var string */
    protected $path;
    /** @var string */
    protected $name;
    /** @var string */
    protected $baseDir;

    /**
     * @return string
     */
    public function getPath()
    {
        return $this->path;
    }

    /**
    * @return string
    */
    public function getBaseDir()
    {
    return $this->baseDir;
    }

    // ... etc
}

2. Bun\Core\FileSystem\Dir

Директория в основном нужна мне сейчас для удобного чтения директорий и проверки их на существование, поэтому представлю только актульаный на сейчас методы в теле класса:


<?php
namespace Bun\Core\FileSystem;

use Bun\Core\Exception\FileSystemException;

/**
 * Class Dir
 *
 * @package Bun\Core\FileSystem
 */
class Dir extends AbstractFileSystemNode
{
    /** @var resource */
    protected $context;

    /**
     * @param $path
     * @param bool $create
     * @param int $mode
     */
    public function __construct($path, $create = false, $mode = 0777)
    {
        $this->path = $path;
        $this->baseDir = $path;
        $this->name = basename($path);

        if($create) {
            $this->create($mode, true);
        }
    }

    // ...

    /**
    * @param null $extension
    *
    * @return string
    */
    public function readFile($extension = null)
    {
        $fileName = @readdir($this->getReadDirContext());
        if(!$fileName) {
            return $fileName;
        }

        $filePath = $this->baseDir .'/'. $fileName;
        if(!is_file($filePath)) {
            return $this->readFile($extension);
        }

        if($extension !== null) {
            if(strtolower($extension) === strtolower(File::getFileExtension($fileName))) {
                return $fileName;
            }

            return $this->readFile($extension);
        }

        if($fileName !== '.' && $fileName !== '..') {
            return $fileName;
        }

        return $this->readFile($extension);
    }

    /**
    * @return string
    */
    public function readDir()
    {
        $dirName = @readdir($this->getReadDirContext());
        if(!$dirName) {
            return $dirName;
        }

        if(!is_dir($this->baseDir .'/'. $dirName)) {
            return $this->readDir();
        }

        if($dirName === '.' || $dirName === '..') {
            return $this->readDir();
        }

        return $dirName;
    }

    /**
    * @return $this
    */
    public function clearReadFileContext()
    {
        $this->context = null;

        return $this;
    }

    /**
    * @return resource
    * @throws \Bun\Core\Exception\FileSystemException
    */
    protected function getReadDirContext()
    {
        if($this->context === null) {
            $this->context = @opendir($this->path);
            if(!$this->context) {
                throw new FileSystemException('Unable to read dir: '. $this->path);
            }
        }

        return $this->context;
    }

Я реализовал методы для чтения из директории только директорий и возможности считывать только файл с определенным расшинением. Просто чтобы исзбавиться от этого утилитного кода в других компонентах системы.

3. Bun\Core\FileSystem\File

Тут тоже все просто и тривиально, я представлю только небольшой кусочек класса:


<?php
namespace Bun\Core\FileSystem;

use Bun\Core\Exception\FileSystemException;

/**
* Class File
*
* @package Bun\Core\FileSystem
*/
class File extends AbstractFileSystemNode
{
    /** @var string|null */
    protected $extension;
    /** @var string */
    protected $content;

    /**
    * @param $path
    * @param bool $create
    * @param int $mode
    */
    public function __construct($path, $create = false, $mode = 0777)
    {
        $this->path = $path;
        $this->baseDir = dirname($path);
        $this->name = basename($path);
        $this->extension = self::getFileExtension($path);

        if($create) {
            $this->create($mode);
        }
    }

    /**
    * @param $mode
    *
    * @throws \Bun\Core\Exception\FileSystemException
    */
    public function create($mode)
    {
        if(!Dir::exists($this->baseDir)) {
            new Dir($this->baseDir, true, $mode);
        }

        if(!self::exists($this->path)) {
            $ok = @touch($this->path);
            if(!$ok) {
                throw new FileSystemException('Unable to touch file: '. $this->path);
            }
            $ok = @chmod($this->path, $mode);
            if(!$ok) {
                throw new FileSystemException('Unable to chmod file: '. $this->path .' in '. $mode);
            }
        }
    }

    /**
    * @param bool $reload
    *
    * @return null|string
    */
    public function getContent($reload = false)
    {
        if($this->content === null || $reload) {
            $this->readContent();
        }

        return $this->content;
    }

    /**
    * @throws \Bun\Core\Exception\FileSystemException
    */
    protected function readContent()
    {
        if(self::isReadable($this->path)) {
            $this->content = @file_get_contents($this->path);
            if($this->content === false) {
                throw new FileSystemException('Unable to read file: '. $this->path);
            }
            return;
        }

        throw new FileSystemException('File: '. $this->path .' is not readable');
    }

    // ...
}

Как видите, чисто утилитный код, ничего особенного, но избавляет нас в дальнейшем от множется повторяющихся проверок на возможные ошибки при работе с файлами, плюс сразу дает возможности отлавливать типизированные исключния при возникших проблемах.


Bun\Core\Config


С зависимостями покончили, теперь переходим к непосредственно реализации самого компонента настроек. Согласно плану пишем первый класс:


<?php
namespace Bun\Core\Config;

/**
 * Class AbstractConfig
 *
 * @package Bun\Core\Config
 */
abstract class AbstractConfig
{
    protected $name = 'abstract';

    protected $config = array();

    /**
     * @return array
     */
    public function getConfig()
    {
        return $this->config;
    }

    /**
    * @return string
    */
    public function getName()
    {
        return $this->name;
    }

    /**
    * @param $param
    *
    * @return mixed|null
    */
    public function get($param)
    {
        if ($param === null) {
            return $this->config;
        }

        if (strpos($param, '.') !== false) {
            $paramParts = explode('.', $param);

            return $this->recursiveGet($paramParts, $this->config);
        }

        return (isset($this->config[$param])) ?
            $this->config[$param] :
            null;
    }

    /**
    * @param array $paramParts
    * @param array $config
    *
    * @return null|mixed
    */
    protected function recursiveGet($paramParts = array(), $config = array())
    {
        $param = array_shift($paramParts);
        if (isset($config[$param])) {
            return (!$paramParts) ?
                $config[$param] :
                $this->recursiveGet($paramParts, $config[$param]);
        }

        return null;
    }
}

Это тот самый абстрактрный класс, от которого будет необходимо наследовать остальные классы настроек. Свойство $name - определяет пространство имен настроек, $config – сами настройки. Также зедсь реализуем публичный метод get(), который и являет собой реализацию dot-нотации доступа к параметрам конфига. Все лаконично и просто – ничег лишнего (как говорится, лучший код тот, который не был написан)

Теперь основной класс. Я выбрал для него название Bun\Core\Config\ApplicationConfig.

Тут код буду сопровожать комментариями прямо в листинге и представлю весь класс, хоть он и довольно большой получился. НО для целостности картины не помешает:


<?php
namespace Bun\Core\Config;

use Bun\Core\Application;
use Bun\Core\Exception\ConfigException;
use Bun\Core\FileSystem\Dir;
use Bun\Core\FileSystem\File;

/**
* Loads and allows access to all configuration parameters in application
*
* Class ApplicationConfig
*
* @package Bun\Core\Config
*/
class ApplicationConfig extends AbstractConfig
{
    /** @var Application */
    protected $application;
    /** @var array */
    protected $configParams = array();

    /**
    * на входе принимаем объект запущенного приложения
    * @param Application $application
    */
    public function __construct(Application $application)
    {
        $this->application = $application;
        $this->load(); // сразу запускаем загрузку настроек
    }

    /**
    * loads application config
    */
    protected function load()
    {
        $this->loadBunConfig(); // загружаем только настройки фреймворка
        $this->loadApplicationsConfig(); // загружаем настройки приложений, в т.ч запущенного сейчас
        $this->loadConfigParams(); // подставляем в настройки параметры из json-файлов
    }

    /**
    * Loads config classes from bun modules
    */
    protected function loadBunConfig()
    {
        $bunDir = new Dir(BUN_DIR); // открываем и читаем директорию кода фреймворка, получая названия модулей
        while($moduleDir = $bunDir->readDir()) {
            // вызываем загурзку настроек конкретного модуля
            $this->loadModuleConfig($bunDir->getBaseDir() .'/'. $moduleDir, 'Bun\\' . $moduleDir);
        }
    }

    /**
    * Loading all applications config and current application config in the end
    */
    protected function loadApplicationsConfig()
    {
        // открываем и читаем директорию приложений
        $srcDir = new Dir(SRC_DIR);
        $currentApplicationDir = '';
        while($moduleDir = $srcDir->readDir()) {
            if($moduleDir !== $this->application->getNameSpace()){
                // если прилоежние на запущено, загружаем его настройки
                // стоит обратить внимание, что настройки модуля и прилоежний грузяться одним методом
                $this->loadModuleConfig($srcDir->getBaseDir() .'/'. $moduleDir, $moduleDir);
            }
            else {
                // определяем запущенное прилоежние]
                $currentApplicationDir = $moduleDir;
            }
        }

        // в последнюю очередь подгружаем настройки активного прилоежния, т.к. они должны быть приоритетными и переопредить все остальные
        $this->loadModuleConfig($srcDir->getBaseDir() .'/'. $currentApplicationDir, $currentApplicationDir);
    }

    /**
    * @throws \Bun\Core\Exception\ConfigException
    */
    protected function loadConfigParams()
    {
        // перезагужаем конфиг с применением считанных параметров из json-файлов
        $reload = array();
        foreach($this->config as $param => $value) {
            if(is_array($value)) {
                // массив читаем рекурсивно этой функцией
                $reload[$param] = $this->loadRecursiveParam($value);
            }
            elseif(strpos($value, ':') === 0) {
                // синтаксис использования параметров настроек имеет вид ":param"
                // т.е. вмето этого выражения будет подставлено значение param из загруженных параметров
                $paramKey = str_replace(':', '', $value);
                if(isset($this->configParams[$paramKey])) {
                    $reload[$param] = $this->configParams[$paramKey];
                }
                else {
                    // если параметр не был определен, то стоит выбросить исключение
                    throw new ConfigException('Parameter '. $paramKey .' does not specified in config params');
                }
            }
            else {
                // скалярные параметры оставляем без изменений
                $reload[$param] = $value;
            }
        }

        // соханяем перезагруженный массив настроек
        $this->config = $reload;
    }

    /**
    * @param $config
    *
    * @return array
    * @throws \Bun\Core\Exception\ConfigException
    */
    protected function loadRecursiveParam($config)
    {
        // тут тоже самое, что в предыдущей функции, только вмето сохранения конфига, мы возвращаем его, т.к. энто рекурсия
        $reload = array();
        foreach($config as $param => $value) {
            if(is_array($value)) {
                $reload[$param] = $this->loadRecursiveParam($value);
            }
            elseif(strpos($value, ':') === 0) {
                $paramKey = str_replace(':', '', $value);
                if(isset($this->configParams[$paramKey])) {
                    $reload[$param] = $this->configParams[$paramKey];
                }
                else {
                    throw new ConfigException('Parameter '. $paramKey .' does not specified in config params');
                }
            }
            else {
                $reload[$param] = $value;
            }
        }

        return $reload;
    }

    /**
    * @param $modulePath
    * @param $moduleNameSpace
    *
    * @throws \Bun\Core\Exception\ConfigException
    */
    protected function loadModuleConfig($modulePath, $moduleNameSpace)
    {
        // тут универсальный загрузчик настроек модуля или приложения
        // по сути парсер директории Config
        $moduleConfigPath = $modulePath .'/Config';
        // проверяем что директория есть
        if(Dir::exists($moduleConfigPath)) {
            // loading config classes
            $configDir = new Dir($moduleConfigPath);
            // читаем из нее php-файлы
            while($configFile = $configDir->readFile('php')) {
                $baseClassName = File::getFileNameWithoutExtension($configFile);
                $configClass = $moduleNameSpace .'\\Config\\' . $baseClassName;
                // исключаем несуществующие классы и абстрактные классы а также самого себя
                if(
                    $baseClassName !== 'AbstractConfig' &&
                    $baseClassName !== 'ApplicationConfig' &&
                    class_exists($configClass)
                ) {
                    /** @var AbstractConfig $configObj */
                    $configObj = new $configClass();
                    // если класс не унаследован от базового, то бросим исключение
                    if($configObj instanceof AbstractConfig) {
                        // иначе даем мердж массивов с заменой загруженных ранее значений
                        $loaded = isset($this->config[$configObj->getName()]) ?
                            $this->config[$configObj->getName()] :
                            array();
                        $merged = array_replace_recursive($loaded, $configObj->getConfig());
                        $this->config[$configObj->getName()] = $merged;
                    }
                    else {
                        throw new ConfigException('Class '. $configClass .' not a config instance');
                    }
                }
            }
            // loading params from json files
            $configDir->clearReadFileContext(); // очищаем контекст чтения директории
            $paramsFile = new File($configDir->getBaseDir() .'/config.json'); // считываем файл параметров
            if(File::exists($paramsFile->getPath())) {
                $this->loadConfigParamsFile($paramsFile); // загружаем из него настройки
            }
            // и тоже самое для файла параметров среды (ENV)
            $paramsFile = new File($configDir->getBaseDir() .'/config_'. $this->application->getEnv() .'.json');
            if(File::exists($paramsFile->getPath())) {
                $this->loadConfigParamsFile($paramsFile);
            }
        }
    }

    /**
    * @param File $paramsFile
    *
    * @throws \Bun\Core\Exception\ConfigException
    */
    protected function loadConfigParamsFile(File $paramsFile)
    {
        // тут добавлена строгая проверка, что json-файлы параметров содежрать валидный json (каламбур, однако)
        $configParams = json_decode($paramsFile->getContent(), true);
        if($configParams !== null && is_array($configParams)) {
            $this->configParams = array_replace_recursive($this->configParams, $configParams);
        }
        else {
            throw new ConfigException('Config parameters file: '. $paramsFile->getPath() .
            ' does not contain valid json encoded parameters');
        }
    }
}

Готово. Можно содать пару тестовых файликов с конфигами и параметрами и писать unit-тесты.


Тестируем написанный код с помощью PHPUnit


Протестировать написанный компонент удобнее всего с помощью автоматизированных PHP unit-тестов. Даже для тех кто никогда эти не занимался, и сейчас я покажу как элементарно это делается, буквально за 10-15 минут я написал практически полноценный тест кейс для компонента конфигурации фреймворка Bun.

1. Устанавливаем PHPUnit

Я просто скачиваю phpunit.phar в корень проекта. Это довольно удобно, т.к. моя IDE автоматически подхватывает его и при написании тестов я могу пользоваться автодополнением и прочими плюшками

PHPUnit in PHP Storm

2. Настраиваем среду для тестирования

Чтобы тестировать классы, тесты должны знать как эти классы загрузить и запустить. Поэтому нам нужно в тесты включать те же автолодеры, что и в само приложение. Чтобы каждый раз в тесте не делать инклуды файлов, я выделил общий bootstrap-файл, который разместил в lib/bun/bun/src/Tests/bootstrap.php:


<?php

$configFile = __DIR__ .'/../../../../../config.php';
if(file_exists($configFile)) {
   require_once $configFile;
}
else {
    define('ENV', 'dev');
}

require_once __DIR__ . '/../../../../autoload.php';

Этот файл обеспечит нам автозагурзку всех нужных классов и констант приложения и фреймворка. Кроме того, тесты фреймворка должны быть независимы от прилоежний, а во фреймвроке у нас только абстрактный класс Bun\Core\Application. Поэтому для тестов я создал общий класс приложения Bun\Tests\TestApplication:


<?php
namespace Bun\Core\Tests;

use Bun\Core\Application;

/**
 * Class TestApplication
 *
 * @package Bun\Core\Tests
 */
class TestApplication extends Application
{
    /**
     * @return string
     */
    public function getNameSpace()
    {
        return __NAMESPACE__;
    }
}

И последний шаг настройки среды тестирования: конфигурируем PHPUnit, чтобы нам удобно было запускать тесты. Я создал в директории lib/bun/bun/src файлик phpunit.xml, с такими настройками:


<phpunit
        bootstrap="Tests/bootstrap.php"
        backupGlobals="false"
        backupStaticAttributes="false"
        convertErrorsToExceptions="true"
        convertNoticesToExceptions="true"
        convertWarningsToExceptions="true"
        syntaxCheck="false"
        processIsolation="false"
        colors="true">
    <testsuites>
        <testsuite name="Bun_Framework">
            <directory>.</directory>
        </testsuite>
    </testsuites>
</phpunit>

Тут у нас основной момент – это автоматическое подключение бустрап файла для всех тестов и путь к директории тестов. Я просто указал все директорию кода фреймворка — PHPUnit сам поймет и найдет нужные ему тесты. В дальнешейм конечно же нужно будет разбить тесты на отдельный сьюты, но пока так вполне сойдет.

3. Пишем тест case

Для начала нужно будет предусмотреть тестовые данные. Я создал два класса настроек Bun\Core\Config\TestConfig1.php и Bun\Core\Config\TestConfig2.php. А также два файла с параметрами настроек: config.json и config_dev.json, т.к. мое приложение запускается в dev-окружении.


<?php
namespace Bun\Core\Config;

/**
 * Class TestConfig2
 *
 * @package Bun\Core\Config
 */
class TestConfig2 extends AbstractConfig
{
    protected $name = 'test1';

    protected $config = array(
        'ns2' => array(
            'param22' => 'ok22',
        ),
        'ns3' => array(
        'params3' => 'ok3',
            'params33' => array(
            'param333' => 'ok333',
                'parameter1' => ':test.parameter1', // тут определяем имена параметров
                'parameter2' => ':test.parameter2',
            ),
        ),
    );
}

И сами параметры:


{
    "test.parameter1": "ok1",
    "test.parameter2": "ok1"
}

Я решил разделить тесты по модулям фреймворка, т.е. в каждом модуле своя директория Tests со своими тестами. Итак создаю класс теста Bun\Core\Tests\ApplicationConfigTest.php


<?php
namespace Bun\Core\Tests;

use Bun\Core\Config\ApplicationConfig;

/**
 * Class ApplicationConfigTest
 *
 * @package Bun\Core\Tests
 */
class ApplicationConfigTest extends \PHPUnit_Framework_TestCase
{
    /**
     * Создаем приложение и объект класса настроек
     * @return ApplicationConfig
     */
    protected function getConfig()
    {
        $app = new TestApplication(ENV);
        $config = new ApplicationConfig($app);

        return $config;
    }

    /**
     * Тестим простой доступ к массиву параметров
     */
    public function testGet()
    {
        $expect = array(
            'param1' => 'ok1',
        );

        $actual = $this->getConfig()->get('test1.ns1');
        $this->assertEquals($expect, $actual);
    }

    /**
    * Тестим доступ через dot-нотацию
    */
    public function testGetDot()
    {
        $expect = 'ok2';
        $actual = $this->getConfig()->get('test1.ns2.param2');

        $this->assertEquals($expect, $actual);
    }

    /**
    * Тестим доступ к настройке, определенной в параметрах config.json
    */
    public function testGetParam()
    {
        $expect = 'ok1';
        $actual = $this->getConfig()->get('test1.ns3.params33.parameter1');

        $this->assertEquals($expect, $actual);
    }

    /**
    * Тестим доступ к настройке, определенной в параметрах среды config_dev.json
    */
    public function testGetEnvParam()
    {
        $expect = 'ok2';
        $actual = $this->getConfig()->get('test1.ns3.params33.parameter2');

        $this->assertEquals($expect, $actual);
    }

    /**
    * Тестим доступ к несуществующему параметру
    */
    public function testGetNull()
    {
        $expect = null;
        $actual = $this->getConfig()->get('some.very.unknown.parameter');

        $this->assertEquals($expect, $actual);
    }
}

А теперь запускаем PHPUnit с заданной конфигурацией и радуемся жизни:

PHPUnit success tests on Bun Framework
P.S. – часть кода и листингов файлов опущена в статье, вы можете просмотреть код в открытом репозитории проекта Bitbucket jakulov/bun. Акутальный коммит на момент написания статьи: aa1b73f

Спасибо за внимание, буду рад замечаниям и вопросам. В следующих статьях буду рассказывать про создание связки Http-компонентов Request/Router/Response.


Автор   

Комментарии (3)    написать


Jacob Akulov

Наварил макарон, однако...

 Ответить   


Сергей Керимов

Доброго времени суток !
Скачал из репозитария проект, запустил тесты, все с ошибкой "Only variables should be passed by reference"

 Ответить   


Yakov Akulov

Сергей Керимов: вполне возможно, проект в разработке, в новой статье, скорее всего будет более менее стабильная сборка.

 Ответить      



Написать комментарий:

Чтобы комментрировать укажите свои данные или войдите через один из социальных сервисов

Загрузка...