Часть 6. Bun Framework MCV - архитектура runtime php-приложения

2015-02-19 в 00:07 Bun Framework PHP

В предыдущих статьях я рассказал о создании базовых компонентов для своего фреймворка: модуль конфигурации приложения, компоненты http: Request & Response, а также интересный компонент – Dependency Injection Container – который позволит нам управлять зависимостями в приложении. Далее, чтобы наше приложение могло что-то делать, необходимо создать компоненты фреймворка, которые отвечают непосредственно за runtime приложения. У нас уже есть класс Bun\Core\Application, который принимает все стартовые запросы нашего приложения, далее мы сделали компонент Router, который позволяет определить какую конкретную функцию приложения вызвал пользователь. Далее нам нужен набор копонент MVC – который позволит непосрдетсвенно реализовать эту самую функцию приложения. Первым компонентом будет Controller - класс, котоому предается управление непосредственно после запуска приложения. Контроллер будет иметь доступ к слою Model (модели), чтобы работать с данными, и также с слою View (отображение) – чтобы отображать информацию для пользователя. Как правило в php-приложениеях в практике MVC под View чаще всего понимаются шаблоны (Templates).
Начнем с того, что представим небольшую схему runtime нашего приложения, основанного на Bun Framework. Bun Framework Runtime


Runtime

На схеме выше хорошо показаны все возможные варианты развития хода выполнения приложения (runtime) с момента создания класса Request и до вывода сообщения пользователю. Архитектура класса Application как раз призвана реализовать данную схему цикла жизни запроса, причем так, чтобы ее можно было легко расширить или переопределить при необходимости внутри своего приложения.


Bun\Core\Controller

Контроллер является стартовой точкой в бизнес-логике приложения. С точки зрения разработчика приложения – здесь начинается обработка пользовательских запросов. Контроллер должен иметь по сути доступ ко всем компонентам системы, чтобы у разработчика была возможность пользоваться различными сервисами и конфигурировать их под свои нижды, получать различные данные из хранилищ, использовать кэширование и рендерить шаблоны для передечи представления пользователю. Как правило, во всех фреймворках пользовательские контроллеры наследуются от базового или астрактного контроллера – в котором уже реализован некий базовый функционал по инциализации, доступу к различным компонентам системы и запуску так называемых экшенов (action). Под экшеном обычно понимают метод котроллера, который отвечает за обработку того или иного роута, т.е. запроса пользователя. Часто для удобства к таким методам всегда приписывают постфикс Action – например, createUserAction.

По подобному принципу буду действовать и я в своем фреймворке Bun – создам базовый абстрактный контроллер, в котором будет реализован некий базовый функционал контроллера, необходимый в большинстве контроллеров.

<?php
namespace Bun\Core\Controller;

use Bun\Core\Exception\NotFoundException;
use Bun\Core\Exception\ResponseException;
use Bun\Core\Http\RunTimer;
use Bun\Core\Container\Container;
use Bun\Core\Http\ResponseInterface;
use Bun\Core\Application;
use Bun\Core\Http\RoutingResult;

/**
 * Base Controller class with simple startup logic
 *
 * Class AbstractController
 *
 * @package Bun\Core\Controller
 */
abstract class AbstractController implements ControllerInterface
{
    /** @var Application */
    protected $application;
    /** @var RunTimer */
    protected $timer;
    /** @var RoutingResult */
    protected $route;
    /** @var string */
    protected $defaultAction = 'index';

    /**
     * @param Application $app
     * @return $this
     */
    public function setApplication(Application $app)
    {
        $this->application = $app;

        return $this;
    }

    /**
     * Implement your controller init logic here
     */
    protected function init(){}

    /**
     * Implement your controller shutdown logic here
     */
    protected function shutdown(){}

    /**
     * @param RoutingResult $route
     * @return ResponseInterface
     * @throws \Bun\Core\Exception\NotFoundException
     * @throws \Bun\Core\Exception\ResponseException
     */
    public function run(RoutingResult $route)
    {
        $this->timer = new RunTimer(true);
        $this->route = $route;
        $this->init();

        $actionName =
            ($this->route->getActionName() ?
                $this->route->getActionName() :
                $this->defaultAction
            )
            . 'Action';

        if (method_exists($this, $actionName)) {
            /** @var ResponseInterface $response */
            $response = call_user_func_array(
                array($this, $actionName),
                $this->route->getActionParams()
            );
            if(!($response instanceof ResponseInterface)) {
                throw new ResponseException(
                    'Method '. get_class($this) .'::'. $actionName .
                    ' should return ResponseInterface instance'
                );
            }
        }
        else {
            throw new NotFoundException($this->route->getActionNotFoundExceptionMessage(), 404);
        }

        $this->shutdown();
        $response->setTimer($this->timer);

        return $response;
    }

    /**
     * @return Container
     */
    public function getContainer()
    {
        return $this->application->getContainer();
    }
}

Для демонстрации работы компонента – создадим небольшой контроллер, наследуясь от только что реализованного класса, в котором реализуем простой возврат простейшего объекта Response.

<?php
namespace Bun\Core\Controller;

use Bun\Core\Http\Response;

/**
 * Class ResponseController
 * @package Bun\Core\Controller
 */
class ResponseController extends AbstractController
{
    /**
     * @return Response
     */
    protected function indexAction()
    {
        return new Response('OK. Response time: '. $this->timer->getRunTime() .' sec.');
    }
}

Для того, чтобы запустить контроллер, необходимо прописать для него роут в RouterConfig

'/bun/response' => array(
    'controller' => 'Bun\\Core\\Controller\\ResponseController'
),

А также дополнить класс Bun\Core\Application кодом запуска контрллера на основе результата работы роутера (пока в коде полностью опустим обработку ошибок, т.к. это отдельная тема для целой статьи - обработка ошибок во фреймворке на уровне приложения):

<?php
    /**
     * Run the application
     */
    public function run()
    {
        $route = $this->getRouter()->route();

        $ok = $this->runController($route);
        if(!$ok) {
            echo('Controller cannot be started');
        }

        return $ok;
    }
    
    protected function runController(RoutingResult $route)
    {
        // TODO: errors
        $controllerClass = $route->getControllerClass();
        if(class_exists($controllerClass)) {
            $controller = new $controllerClass;
            if($controller instanceof ControllerInterface) {
                $response = $controller->setApplication($this)->run($route);
                if($response && $response instanceof ResponseInterface) {
                    foreach($response->getHeaders() as $code => $header) {
                        if($code >= 200) {
                            header('HTTP 1.1/ ' . $code .' '. $header);
                        }
                        else {
                            header($header);
                        }
                    }
                    echo $response->getContent();

                    return true;
                }
            }
        }

        return false;
    }
Response

Теперь можно увидеть результат работы приложения. Вот мы и реализовали комонент Controller из MVC, но в него сейчас примешан слой отображения данных View, а слой модели по сути отсутствуют - данные генерируютс прям в контроллере, что тоже плохо. Будем работать дальше!


Bun\Core\Model

В данной статье я не буду много рассказывать о слое модели. Вставлю пару слов про то, как я планирую их реализовать в Bun Framework. Подобно контроллеру я реализую некий абстрактный класс модели, который будет реализовывать самые базовые вещи в модели. Затем будет реализован сервис – ObjectMapper, который будет преобразовывать объект модели (со всеми его связями с другими моделями) в массив данных, пригодный к сохранению в любом хранилище (базе данных). Данный сервис будет абстрагирован от способа хранения данных – при этом он будет зависеть от определенного сервиса: Storage. Storage – это некая абстрактная реализация интерфейса хранилища, или другими словами драйвера доступа к базе данных.

Я постараюсь реализовать маппер объектов так, чтобы он не зависил от конкретного хранилища, таким образом мы бы могли удобно переключаться между различными движками БД или же вовсе использовать для хранения объектов файлы. В следующих статьях про маппер объектов я планирую реализовать и небольшой сервис для реализации базы данных на PHP на основе файловой системы. Я планирую включить его в Core модуль своего фреймворка, чтобы приложение на Bun можно было запустить в практически любой среде, где есть PHP.


View

Компонент представлений, в PHP-приложениях, как я уже говорил, часто реализуют просто в виде механизма шаблонов с небольшой надсткойкой над ними. Данную надстройку я выделю в отдельный сервис bun.core.view.service, который позволит нам в контроллере рендерить нужные вьюхи. Также я могу добиться абстрагирования от различных движков шаблонизаторов – для демонстрации этого я включу в базовый модуль Core поддержку нативных php-шаблонов и шаблонизатора Twig, так, чтобы вызов шаблона внутри контроллеры выглядел одинаково при использовании любого из них. (Поддержка шаблонизации Twig вынесена в отдельну статью для удобства)

Итак, сервис Bun\Core\View\ViewService, реализующий рендеринг шаблонов из файлов или по строке. Для начала определяем его интерфейс:

<php
namespace Bun\Core\View;

/**
 * Class ViewServiceInterface
 *
 * @package Bun\Core\View
 */
interface ViewServiceInterface
{
    /**
     * @param $template
     * @param array $context
     * @param ViewEngineInterface|string $engine
     * @return string
     */
    public function render($template, $context = array(), $engine = null);

    /**
     * @param $string
     * @param array $context
     * @param ViewEngineInterface|string $engine
     * @return string
     */
    public function renderString($string, $context = array(), $engine = null);

    /**
     * @param ViewEngineInterface|string $engine
     * @return $this
     */
    public function setDefaultEngine($engine);

    /**
     * @param $name
     * @param ViewEngineInterface $engine
     * @return $this
     */
    public function addEngine($name, ViewEngineInterface $engine);

    /**
     * @param ViewEngineInterface|string $name
     * @return ViewEngineInterface
     */
    public function getEngine($name);

    /**
     * @param bool $returnName
     * @return ViewEngineInterface
     */
    public function getDefaultEngine($returnName = false);
}

Интерфейс довольно прост: имеем методы render и renderString для рендера шаблонов из файла и строки соотв. В качестве параметров можно передать $engine - т.е. прямо указать каким движком рендерить данный шаблон. При чем можно указать как имя движка в конфигурации (об этом далее), так и непосредственно передать готовый объект типа ViewEngineInterface. Об эттиъ объектах далее. Подразумевается, что сам по себе ViewService не будет выполнять никаких функций, а только выбирать нужный движок и запускать в нем рендеринг, а далее просто отдавать полученный результат. Вся реализации работы с шаблонами ложится на класс движка шаблона. Итак его интерейс Bun\Core\View\ViewEngineInterface

<php
namespace Bun\Core\View;

/**
 * Interface ViewEngineInterface
 *
 * @package Bun\Core\View
 */
interface ViewEngineInterface
{
    /**
     * @param array $templateDirs
     * @param array $options
     */
    public function __construct($templateDirs = array(), $options = array());

    /**
     * @param $dir
     * @return $this
     */
    public function addTemplateDir($dir);

    /**
     * @param array $options
     * @return $this
     */
    public function setOptions($options = array());

    /**
     * @param $name
     * @param $value
     * @return $this
     */
    public function setOption($name, $value);

    /**
     * @param $key
     * @param $value
     * @return $this
     */
    public function addContext($key, $value);

    /**
     * @param array $context
     * @return $this
     */
    public function setContext($context = array());

    /**
     * @param $template
     * @param array $context
     * @return string
     * @throws ViewException
     */
    public function render($template, $context = array());

    /**
     * @param $string
     * @param array $context
     * @return string
     * @throws ViewException
     */
    public function renderString($string, $context = array());

    /**
     * @param null $key
     * @return mixed
     */
    public function getContext($key = null);
}

Тут уже немного сложнее. Интерфейс довольно объемный, чтобы упростить его реализацию далее я создам абстрактный класс Bun\Core\View\AbstractViewEngine - который реализует рутиные функции, вроде добавления опций, директорий шаблонов, чтения/записи контекста и т.п. Основными методами к реализации останутся лишь render и renderString. Так что, в данной статье опущу реализацию рутиных фукнций и сразу перейду к классу движка нативных php-шаблонов. Bun\Core\View\NativeViewEngine

<php
namespace Bun\Core\View;

/**
 * Class NativeViewEngine
 *
 * @package Bun\Core\View
 */
class NativeViewEngine extends AbstractViewEngine
{
    /** @var string */
    protected $defaultExtension = 'phtml';

    const DEFAULT_TMP_DIR = '/tmp';

    /**
     * @param $template
     * @param array $context
     * @return string
     * @throws ViewException
     */
    public function render($template, $context = array())
    {
        $filename = $this->findTemplate($template);
        if($filename !== null) {
            $data = array_replace_recursive($this->context, $context);

            return $this->renderFile($filename, $data);
        }

        throw new ViewException('Unable to find template "'. $template .'" in directories: '. join(', ', $this->dirs));
    }

    /**
     * @param $filename
     * @param array $data
     * @return string
     */
    protected function renderFile($filename, $data = array())
    {
        ob_start();
        extract($data);

        require $filename;

        $content = ob_get_contents();
        ob_end_clean();

        return $content;
    }

    /**
     * @param $string
     * @param array $context
     * @return string
     * @throws ViewException
     */
    public function renderString($string, $context = array())
    {
        $tmpDir = $this->getTmpDir();
        $tmpFile = $tmpDir . DIRECTORY_SEPARATOR . mt_rand(100000, 200000) . '_'. md5(microtime()) .'.' . $this->defaultExtension;
        if(is_writable($tmpDir)) {
            file_put_contents($tmpFile, $string);
            $data = array_replace_recursive($this->context, $context);

            $content = $this->renderFile($tmpDir, $data);
            unlink($tmpFile);

            return $content;
        }

        throw new ViewException('Unable to write to tmp dir: '. $tmpDir);
    }

    /**
     * @return string
     * @throws ViewException
     */
    protected function getTmpDir()
    {
        $dir = (isset($this->options['tmp_dir']) ? $this->options['tmp_dir'] : self::DEFAULT_TMP_DIR) . DS . 'bun_native';
        if(!is_dir($dir)) {
            $ok = mkdir($dir, 0777, true);
            if(!$ok) {
                throw new ViewException('Unable to create tmp_dir: "'. $dir .'"');
            }
        }

        return $dir;
    }
}

Код компонента рендеринга нативного PHP-шаблона на самом деле прост. Все сводится к использованию extract и инклуду нужного файла шаблона. Если идет рендеринт строки, то я выбрал вариант создания временного файла. Хотя можно использовать и eval(), но не думаю, что кто-то любит использовать php в качестве строки-шаблона, поэтому реализации имеется больше для совместимости с интерфейсом.


Конфигурация компонента View

Осталась последняя важная часть реализации реализации комонента шаблонов (View) - это конфигурация. Будем также использовать встроенный в наш фреймворк компонент конфигурации. Создаем класс Bun\Core\Config\ViewConfig.

<php
namespace Bun\Core\Config;

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

    protected $config = [
        'engines' => [
            'native' => 'Bun\\Core\\View\\NativeViewEngine'
        ],
        'default_engine' => 'native',
        'template_dirs' => ['Layout'],
        'engine_options' => [
            'tmp_dir' => 'tmp/view',
            'cache_dir' => 'cache/view',
        ],
    ];
}

Как вы видите, пока у нас есть только один движок шаблонизации, он же задан как дефолтный. Задан массив директорий с шаблонами (если директория начинается со "/", то она считается как абсолютный путь, иначе подразумеваеся, что эта директория внутри директории приложения или в lib/bun/bun/src/Core. Отедльно задаются опции движков, весь этот массив передается в конструктор класса движка при его инициализации.

А теперь, собираем все вместе - встречайте, реализация класса Bun\Core\View\ViewService.
<php
namespace Bun\Core\View;

use Bun\Core\Config\ApplicationConfig;
use Bun\Core\Config\ConfigAwareInterface;

/**
 * Class ViewService
 *
 * @package Bun\Core\View
 */
class ViewService implements ViewServiceInterface, ConfigAwareInterface
{
    /** @var array */
    protected $engines = array();
    /** @var ViewEngineInterface */
    protected $defaultEngine;
    /** @var ApplicationConfig  */
    protected $config;
    /** @var array  */
    protected $engineOptions = [];
    /** @var array  */
    protected $templateDirs = [];
    /** @var string */
    protected $defaultEngineName;

    /**
     * @param ApplicationConfig $config
     */
    public function setConfig(ApplicationConfig $config)
    {
        $this->config = $config;
        $this->initTemplateDirs();
        $this->initViewEngines();
    }

    /**
     * @throws ViewException
     */
    protected function initViewEngines()
    {
        $engineOptions = $this->config->get('view.engine_options');
        $engines = $this->config->get('view.engines');
        $this->defaultEngineName = $this->config->get('view.default_engine');
        foreach($engines as $name => $class) {
            if(class_exists($class)) {
                $objReflection = new \ReflectionClass($class);
                if($objReflection->implementsInterface(ViewEngineInterface::class)) {
                    $engine = $objReflection->newInstance([$this->templateDirs, $engineOptions]);
                    unset($objReflection);
                    $this->engines[$name] = $engine;
                    if(!$this->defaultEngineName) {
                        $this->defaultEngineName = $name;
                        $this->defaultEngine = $engine;
                    }
                }
                else {
                    throw new ViewException(
                        'View engine class "'. $class .'" should implement interface: ' . ViewEngineInterface::class
                    );
                }
            }
            else {
                throw new ViewException('View engine class "'. $class .'" does not exists');
            }
        }

        if(!$this->defaultEngine && $this->defaultEngineName) {
            if(isset($this->engines[$this->defaultEngineName])) {
                $this->defaultEngine = $this->engines[$this->defaultEngineName];
            }
            else {
                throw new ViewException(
                    'Unable to find default view engine with name = "'. $this->defaultEngineName .'"'
                );
            }
        }
        elseif(!$this->defaultEngine) {
            throw new ViewException('Unable to init any of configured template engines!');
        }
    }

    /**
     * @throws ViewException
     */
    protected function initTemplateDirs()
    {
        $templateDirs = $this->config->get('view.template_dirs');
        $bunDirs = [];
        foreach($templateDirs as $dir) {
            if(strpos($dir, '/') === 0) {
                if(is_dir($dir) && is_readable($dir)) {
                    $this->templateDirs[] = $dir;
                }
                else {
                    throw new ViewException('Configured template dir "'. $dir .'" does not exists or is not readable');
                }
            }
            else {
                $appDir = SRC_DIR . DS . $this->config->getApplicationNamespace() . DS . $dir;
                $this->templateDirs[] = $appDir;
                $bunDir = realpath(__DIR__ . DS . '..' . DS . $dir);
                if($bunDir && is_dir($bunDir)) {
                    $bunDirs[] = $bunDir;
                }
            }
        }

        $this->templateDirs = array_values(array_unique($this->templateDirs + $bunDirs));
    }

    /**
     * @param $template
     * @param array $context
     * @param ViewEngineInterface|string $engine
     * @throws ViewException
     * @return string
     */
    public function render($template, $context = array(), $engine = null)
    {
        $useEngine = $this->getEngine($engine);
        if($useEngine) {
            return $useEngine->render($template, $context);
        }

        throw new ViewException('Unable to find view engine with name = "'. $engine .'"');
    }

    /**
     * @param $string
     * @param array $context
     * @param ViewEngineInterface|string $engine
     * @throws ViewException
     * @return string
     */
    public function renderString($string, $context = array(), $engine = null)
    {
        $useEngine = $this->getEngine($engine);
        if($useEngine) {
            return $useEngine->renderString($string, $context);
        }

        throw new ViewException('Unable to find view engine with name = "'. $engine .'"');
    }

    /**
     * @param null $name
     * @return ViewEngineInterface|null
     */
    public function getEngine($name = null)
    {
        if($name instanceof ViewEngineInterface) {
            return $name;
        }

        if($name === null) {
            return $this->defaultEngine;
        }

        return isset($this->engines[$name]) ? $this->engines[$name] : null;
    }

    /**
     * @param $name
     * @param ViewEngineInterface $engine
     * @return $this
     */
    public function addEngine($name, ViewEngineInterface $engine)
    {
        $this->engines[$name] = $engine;
    }

    /**
     * @param ViewEngineInterface|string $engine
     * @return $this
     * @throws ViewException
     */
    public function setDefaultEngine($engine)
    {
        if($engine instanceof ViewEngineInterface) {
            foreach($this->engines as $name => $eng) {
                if($eng === $engine) {
                    $this->defaultEngine = $engine;
                    return;
                }
            }

            throw new ViewException('Unable to set ' . get_class($engine) . '#' . spl_object_hash($engine) .' as default engine (not found in added engines: '. join(', ', array_keys($this->engines)) .')');
        }
        elseif(isset($this->engines[$engine])) {
            $this->defaultEngine = $this->engines[$engine];
            return;
        }

        throw new ViewException('Unable to set ' . $engine .' as default engine (not found in added engines: '. join(', ', array_keys($this->engines))  .')');
    }

    /**
     * @param bool $returnName
     * @return ViewEngineInterface|null|string
     */
    public function getDefaultEngine($returnName = false)
    {
        if(!$returnName) {
            return $this->defaultEngine;
        }

        foreach($this->engines as $name => $eng) {
            if($eng === $this->defaultEngine) {
                return $name;
            }
        }

        return null;
    }
}

Довольно много места заняла инициализация конфигурации, создание движков и т.п. Остальная реализация довольно тривиальная. В реализации сервиса можно обратить внимание на использование aware-интерфейсов, принцип работы которых я описал в статье про Dependency Injection Container в PHP

В завершении реализации и перед написание тестов, добавим изменения в наш выше созданный контроллер, чтобы упростить работу с ViewService и протестировать наглядно работу MVC в Bun Framework. Добавим пару методов в базовый контроллер: рендер шаблона из файла и получение ViewService

<php
    /**
     * @param $template
     * @param $context
     * @param $engine
     * @return string
     * @throws \Bun\Core\View\ViewException
     */
    protected function render($template, $context, $engine = null)
    {
        if($engine === null && $this->templateEngine !== null) {
            $engine = $this->templateEngine;
        }

        if($this->renderContext) {
            $context = array_replace_recursive($this->renderContext, $context);
        }

        return $this->getViewService()->render($template, $context, $engine);
    }

    /**
     * @return \Bun\Core\View\ViewService
     */
    protected function getViewService()
    {
        return $this->getContainer()->get('bun.core.view.service');
    }

Наглядная демонстрация MVC в PHP

Итак, у нас уже реализованы все компоненты для того, чтобы запустить в контроллере нужный метод и вернуть в нем ответ. Теперь добавим к этому возможность использовать слой представления (View), который описывает как выглядят возвращаемые контроллером данные. Создадим контроллер Bun\Core\Controller\BunController, на который направим URL главной страницы через роутер.

<php
namespace Bun\Core\Controller;

use Bun\Core\Http\Response;
use Bun\Framework;

/**
 * Class BunController
 * @package Bun\Core\Container
 */
class BunController extends AbstractController
{
    /**
     * @return Response
     */
    protected function indexAction()
    {
        $data = [
            'name' => 'Bun Framework',
            'version' => Framework::VERSION,
        ];

        return new Response($this->render('index', $data));
    }
}

Контроллер как видим очень простой. Теперь создаем директорию Layout, для шаблонов и кладем туда index.phtml:

<!DOCTYPE html>
<html>
<?
/**
 * @var $name
 * @var $version
 */
?>
<head>
<meta charset="utf-8">
<title><?=$name?></title>
</head>
<body>
<h1><?=$name?></h1>
<p>Version: <?=$version?></p>
</body>
</html>

Вот и все, в результате получим html-страницу с выведенным переменным, заданными в контроллере.



Testing


Для того, чтобы избавиться в дальнейшей разработке фреймворка от регрессий, создадим несколько простых unit-тестов. Данный пример практически полностью покрывает тестами функциональность созданного нами сервиса ViewService:

<php
namespace Bun\Core\Tests\View;

use Bun\Core\Container\Container;
use Bun\Core\View\ViewService;
use Bun\Core\Tests\TestApplication;

/**
 * Class TestViewService
 * @package Bun\Core\Tests\View
 */
class ViewServiceTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @return TestApplication
     */
    protected function getApp()
    {
        return new TestApplication(ENV);
    }

    public function testAddEngine()
    {
        $viewService = $this->getViewService();
        $engine = new TestViewEngine();
        $viewService->addEngine('test', $engine);

        $this->assertEquals($engine, $viewService->getEngine('test'));
    }

    public function testSetDefaultEngine()
    {
        $viewService = $this->getViewService();
        $engine = new TestViewEngine();
        $viewService->addEngine('test', $engine);
        $viewService->setDefaultEngine('test');

        $this->assertEquals($engine, $viewService->getDefaultEngine());

        $viewService->setDefaultEngine($engine);
        $this->assertEquals($engine, $viewService->getDefaultEngine());

        $this->assertEquals('test', $viewService->getDefaultEngine(true));
    }

    public function testRender()
    {
        $template = 'Hi, %test%';
        $data = array(
            'test' => 'View!',
        );

        $expect = str_replace('%test%', $data['test'], $template);

        $viewService = $this->getViewService();
        $engine = new TestViewEngine();
        $viewService->addEngine('test', $engine);

        $actual = $viewService->render($template, $data, 'test');

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

    public function testRenderString()
    {
        $template = 'Hi, %test2%';
        $data = array(
            'test2' => 'View2!',
        );

        $expect = str_replace('%test2%', $data['test2'], $template);

        $viewService = $this->getViewService();
        $engine = new TestViewEngine();
        $viewService->addEngine('test', $engine);

        $actual = $viewService->render($template, $data, 'test');

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

    /**
     * @return ViewService
     */
    protected function getViewService()
    {
        $container = Container::getInstance($this->getApp()->getConfig());

        return $container->get('bun.core.view.service');
    }
}

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

<php
namespace Bun\Core\Tests\View;

use Bun\Core\View\AbstractViewEngine;

/**
 * Class TestViewEngine
 * @package Bun\Core\Tests\View
 */
class TestViewEngine extends AbstractViewEngine
{
    /**
     * @param $template
     * @param array $context
     * @return mixed
     */
    public function render($template, $context = [])
    {
        $str = $template;
        foreach($context as $k => $v) {
            $str = str_replace('%'. $k .'%', $v, $str);
        }

        return $str;
    }

    /**
     * @param $string
     * @param array $context
     * @return mixed
     */
    public function renderString($string, $context = [])
    {
        return $this->render($string, $context);
    }
}

Итак, мы получили полноценную реализацию MVC в нашем PHP-фреймворке. Продемонстрировали работоспособности и гибкость, а также частично покрыли компонент View тестами. Спасибо, если осилили такую длинную статью :)


Автор   

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


Pavel Patrin

Ну вот нахера, Яков, нахера в очередной раз решать одни и те же проблемы?

 Ответить   


Yakov Akulov

Pavel Patrin: ну нравится мне реализовать самому архитектурные компоненты, разобраться с паттернами вживую.
Вот, например

 Ответить      


Alex Pac

Я не понимаю зачем использовать неймспейсы?

Код с ними превращается в кашу, так как неймспейсы скрывают все префиксы.

Если бы неймспейсов не было то код вероятно был нагляднее чем голые имена без префиксов.

Посмотрите на ObjC язык и его фреймворки. Там нет неймспейсов и всегда видно откуда каждая сущность взялась.

-----

namespace Bun\Core\Tests\View;

use Bun\Core\View\AbstractViewEngine;

---

Что вот это за куча-мала?

Почему не написать ?

class TestViewEngine extends BunCoreViewAbstractViewEngine и все!



 Ответить   


Yakov Akulov

Alex Pac: так раньше и делали, взять тот же фреймворк Zend.
Но вот что в этом неудобного, с моей точки зрения:

1. Слишком длинные имена в коде. Одно дело вверху файла написать их один раз в use, другое дело когда у тебя по всему коду эти хреновы BunCoreViewAbstractViewEngine - читать и понимать неудобно.

2. Имя класса должно отражать его суть, а неймспейс просто недопускать конфликта имен. Вот я решил сделать проект с использование Zend Framework. И уменя по всему коду Zend_Mail, Zend_DB и т.п. Префиксы не помогают в понимании кода, не говорят о том, для чего класс. И пишутся лишь для избежания конфликтов имен. Но ведь проще объявить один раз namespace, и не повторяться при каждом использовании класса. Тем более современные IDE для написания PHP кода автоматизируют создание use-директив.

Т.е. по сути при написании кода в PHPStorm, я даже не замечаю все эти длинные Bun\Core\View\AbstractViewEngine, я просто пишу имя класса AbstractViewEngine, а IDE автоматически вынесет длинный неймспейс в блок use.

 Ответить      


Alex Pac

> Alex Pac: а что, если вам все-таки нужно создавать систему с высокой сложностью?
Ну вот ваш проект живет и активно развивается год, два, семь лет. У вас over 1000 классов

Если проект становиться монстром, значит либо он приносит деньги либо подвергся деградации и умирает.

В любом из этих случаем нужно из проекта убрать неиспользуемые части и привести его опять к простой структуре. Либо разделить его на части на маленькие независимые компоненты.

То есть увеличение сложности проекта в большинстве случаев ведет его к гибели и разделение на части. Процветать сложная структура не может.


 Ответить      


Yakov Akulov

Alex Pac: все красиво излагаете, но поверьте, чаще всего проект нельзя поделить именно на НЕЗАВИСИМЫЕ части. Большие проекты естественно разбиваются на подсистемы, но зависимости между ними остаются и никуда подеваться не могут, т.к. это один проект, одна инфорационная система. И работать даже с небольшой его частью, означает учитывать и использовать и остальные компоненты.

И пространства имен тут именно помогают изначально структурировать проект, так, чтобы затем его было проще делить на части.

И еще раз повторюсь - имя класса не должно говорить о том, в какой фреймворк входит этот класс, а лишь назначение этого касса.
Если я использую Zend_Mail - мне важно, что это обретка над email, и не важно, что она написана в Zend.

 Ответить      


Alex Pac

Yakov Akulov: если проект монстр и приносит деньги, то остается только пожелать ему успеха. Но монетизация проекта может преследовать цели которые не совпадают с целей сообщества OpenSource. И исходники в конечном счете будут закрыты и преданы забвению.

 Ответить      


Yakov Akulov

Alex Pac: ахахахаха,
то-то я смотрю в OpenSource сообществе все самые популярные фреймворки образумились и пишут префиксы в своих классах.
Спуститесь на землю и не пишите бред.

 Ответить      


Alex Pac

> всему коду эти хреновы BunCoreViewAbstractViewEngine - читать и понимать неудобно

Как раз такие длинные имена очень дисциплинируют и не дают создать системы с высокой сложностью.

А неймспейсы наоборот позволяют хоть 10 уровней вложенности делать без ущерба для имен. Но нужно ли это?

Если у вас BunCoreViewAbstractViewEngine в коде то нужно подумать не пишите ли Вы абстракцию ради абстракции? Вместо того чтобы создавать реально полезные вещи.

 Ответить   


Yakov Akulov

Alex Pac: а что, если вам все-таки нужно создавать систему с высокой сложностью?
Ну вот ваш проект живет и активно развивается год, два, семь лет. У вас over 1000 классов. Без неймспейсов вы получите отсутствие формализованной структуры классов, опираться на префиксы и рассовывать и искать файлы по директориям вручную - будет тем еще геммороем. Придется извращаться с автозагрузкой. И тут большой вопрос, что получится сложнее - архитектура с или без пространств имен.
А вот следование простому стандарту PSR-4 избавит от этого всего.

 Ответить      


Alex Pac

Если неймспейсы и использовать, то только для указания адресов папок в которой лежит класс

А само наименования классов должно все же использовать префиксы.
Это сделает возможным увеличить сложность структуры и сохранит порядок принадлежности имен классов

То есть имеет место быть комбинированный подход к использованию префиксов и неймспейсов одновременно.

Конкретно по вашему случаю

-----

namespace Bun\Core\Tests\View; //

 Ответить   


Alex Pac

Если неймспейсы и использовать, то только для указания адресов папок в которой лежит класс

А само наименования классов должно все же использовать префиксы.
Это сделает возможным увеличить сложность структуры и сохранит порядок принадлежности имен классов

То есть имеет место быть комбинированный подход к использованию префиксов и неймспейсов одновременно.

Конкретно по вашему случаю

-----
namespace Bun\Core\Tests\View; //

 Ответить   


Alex Pac

Ваша система комментариев не поддерживает простыни а жаль

 Ответить   


Yakov Akulov

Alex Pac: хотите писать простыни, заведите свой блог))

 Ответить      


Alex Pac

Если неймспейсы и использовать, то только для указания адресов папок в которой лежит класс

А само наименования классов должно все же использовать префиксы.
Это сделает возможным увеличить сложность структуры и сохранит порядок принадлежности имен классов

То есть имеет место быть комбинированный подход к использованию префиксов и неймспейсов одновременно.

Конкретно по вашему случаю

 Ответить   


Alex Pac

-----
namespace Bun\Core\Tests\View; //

 Ответить   


Alex Pac

-----
namespace Bun\Core\Tests\View; // здесь все хорошо

use Bun\Core\View\AbstractViewEngine; // здесь тоже

// несмотря на кажущуюся сложность, нейспейсы просто указывают в какой папке лежит класс не более.

// А вот тут имеют место быть классы без префиксов

class TestViewEngine extends AbstractViewEngine

// Как сделать лучше?
// Вероятно вот так:

class BunTestViewEngine extends BunAbstractViewEngine

 Ответить   


Alex Pac

Использование префикса перед именем класса однозначно показывает компонентном какой системы является класс.
Использование без префикса, несмотря на наличие неймспейсов приводит к путанице при чтении кода.

Так как классы без префиксов подсознательно воспринимаются как стандартные, а не как часть фреймворка.

 Ответить   


Yakov Akulov

Alex Pac: суть фреймворком как раз таки в том, чтобы дополнить систему тем, чего в ней не хватает.
Классы фреймворка и должны по сути восприниматься как стандартные.
Не зря же говорят пишу на Symfony (фреймворк выбирается как платформа для приложения).

И если у вас в системе уже есть один компонент, делающий определенные функции, например рендер шаблонов Bun\Core\View\ViewService, логично во всем проекте его воспринимать как ViewService, потому что мне не нужен другой компонент выполняющей такие же функции. Абсолютно нет смысла каждый раз писать BunCoreViewService.

 Ответить      


Alex Pac

прекращаем холивар. Я разобрался и свои мысли выразил тут:

Насчет префиксов пока сомневаюсь, скорее который 2-3 буквенный всеже не помешает, в остальном namespace это правильное решение.

http://www.cyberforum.ru/php-oop/thread1470778.html

 Ответить   



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

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

Загрузка...