在默认视图之前加载客户端视图(如果存在)


Loading clients views before default views if exists

在我的项目(BtoB项目(中,我有一个全局应用程序,里面有很多模块。每个模块都为我的所有客户提供通用功能。

我在根目录中还有一个客户端文件夹,在其中,我有所有客户端的特殊性,在他们的文件夹中。这些文件夹不是模块。所以它们没有加载 Zf2。我通常用抽象工厂加载这些特性。

这个架构遵循的是我目前拥有的:

-   clients
    -   clientOne
        -   Invoice
        -   Cart
        -   Orders
    -   clientTwo
        -   Invoice
        -   Orders
    -   clientThree
        -   Reporting
-   module
    -   Application
    -   CartModule
    -   InvoiceModule
    -   OrdersModule
    -   Reporting

我的客户想要一些自定义视图,有时,他们要求我们提供这些视图。但是我的应用程序给出了所有这些的共同观点。我必须修改此体系结构以加载客户端视图(如果存在(或加载公共视图。

为了处理这种情况,我想在每个客户端文件夹中都有这个:

-   client
    -   clientOne
        -   Invoice
        -   Cart
            -   View
                - cartView.phtml
        -   Orders

编辑:

在一些很好的答案(@AlexP和@Wilt(之后,我尝试实现这个解决方案:

所以我有一个客户策略;它的工厂是这样的:

<?php
namespace Application'View'Strategy;
use Zend'ServiceManager'FactoryInterface;
use Zend'ServiceManager'ServiceLocatorInterface;
use Application'View'Resolver'TemplateMapResolver;
use Zend'View'Resolver;
class ClientStrategyFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $viewRenderer = $serviceLocator->get('ViewRenderer');
        $session = new 'Zend'Session'Container('Session');
        $map = $serviceLocator->get('config')['view_manager']['template_map'];
        $resolver = new Resolver'AggregateResolver();
        $map = new TemplateMapResolver($map, $this->clientMap($session->offsetGet('cod_entprim')));
        $resolver
            ->attach($map)
            ->attach(new Resolver'RelativeFallbackResolver($map));
        $viewRenderer->setResolver($resolver);
        return new ClientStrategy($viewRenderer);
    }

    /**
     * permet de retourner le namespace du client selectionné avec la relation codpriml / nom de dossier
     * @return array
     */
    public function clientMap($codprim)
    {
        $clients = array(
            21500 => 'clientOne',
            32000 => 'clientTwo',
            // ..
        );
        return (isset($clients[$codprim])) ? $clients[$codprim]: false;
    }
}

我的clientMap方法允许我加载我的客户端文件夹,以及它可能在其中的视图,如下所示:

class ClientOne
{
    /**
     * get The main Code
     * @return integer
     */
    public function getCodEntPrim()
    {
        return 21500;
    }
    /**
     * Load all customs views
     * @return array
     */
    public function customViews()
    {
        return array(
            'addDotations' => __DIR__ . '/Dotations/view/dotations/dotations/add-dotations.phtml',
        );
    }
    /**
     * GetName 
     * @return string
     */
    public function getName()
    {
        return get_class();
    }
}

因此,当涉及到我的模板地图解析器来完成他的工作时,我这样做:

<?php
namespace Application'View'Resolver;
class TemplateMapResolver extends 'Zend'View'Resolver'TemplateMapResolver
{
    /**
     * Client name to use when retrieving view.
     *
     * @param  string $clientName
     */
    protected $clientName;
    /**
     * Merge nos vues avec celle clients avant de repeupler l'arrayMap global
     * @param array $map [description]
     */
    public function __construct(array $map, $client)
    {
        $this->setClientName($client);
        if ($this->getCLientName()) {
            $map = $this->mergeMap($map);
        }
        parent::__construct($map);
    }
    /**
     * Merge les map normales avec les map clients, pas propre ?
     * @param  array $map
     * @return array
     */
    public function mergeMap($map)
    {
        $name = $this->getClientName() . '''' . $this->getClientName() ;
        $class = new $name;
        $clientMap = $class->customViews();
        return array_replace_recursive($map, $clientMap);
    }
    /**
     * Retrieve a template path by name
     *
     * @param  string $name
     * @return false|string
     * @throws Exception'DomainException if no entry exists
     */
    public function get($name)
    {
        return parent::get($name);
    }
    /**
     * Gets the Client name to use when retrieving view.
     *
     * @return string
     */
    public function getClientName()
    {
        return $this->clientName;
    }
    /**
     * Sets the Client name to use when retrieving view.
     *
     * @param mixed $clientName the client name
     *
     * @return self
     */
    public function setClientName($clientName)
    {
        $this->clientName = $clientName;
        return $this;
    }
}

我尝试了很多东西,这有效,但出现了一些问题:

  • 我的template_path_stack不再起作用了,所以我的很多观点都被打破了。
  • 我认为这完全是一团糟,这样做,那样做。
  • 难以维护。
  • 我更好地理解它是如何工作的,但我仍然无法以好的方式实现它。

如果您真的想这样做(我不确定这是否是最好的方法(,那么您可以使用自定义逻辑扩展TemplateMapResolver并将其设置在Renderer实例中。

创建自定义类:

<?php
Application'View'Resolver
class TemplateMapResolver extends 'Zend'View'Resolver'TemplateMapResolver
{
    /**
     * Client name to use when retrieving template.
     *
     * @param  string $clientName
     */
    protected $clientName; 
    /**
     * Retrieve a template path by name
     *
     * @param  string $name
     * @return false|string
     * @throws Exception'DomainException if no entry exists
     */
    public function get($name)
    {
        if ($this->has($clientName . '_' . $name)) {
            return $this->map[$clientName . '_' . $name];
        }
        if (!$this->has($name)) {
            return false;
        }
        return $this->map[$name];
    }
}

现在像这样:

$resolver = new TemplateMapResolver();
$resolver->setClientName($clientName);
// Get the renderer instance 
$renderer->setResolver($resolver);

您可能仍需要注意在解析程序中设置地图。也许您可以从旧的解析器中获取它?我不确定...那是让你知道的。这只是为了让你走上正确的道路。

因此,如果您将cart_view设置为模板,它将首先尝试获取client_name_cart_view如果未找到,它将设置cart_view

更新

如果你想把它提升到一个新的水平,那么你能做的是创建一个自定义视图模型,例如扩展普通ViewModel类的ClientViewModel
ClientViewModel的构造函数同时采用客户端和模板名称:

new ClientViewModel($client, $template, $variables, $options);

$variables$options是可选的,可以传递给parent::__construct(正常ViewModel的构造函数(

下一步是创建一个 Application'View'ClientStrategy .

此策略在渲染事件上连接,在此策略中,您可以添加具有自定义TemplateMapResolver集的ViewRenderer实例。在渲染期间,您可以从ViewModel获取客户端,并使用此客户端在TemplateMapResolver中找到正确的模板。

更多细节可以在网上找到,有例子。例如,在此处检查。

优点是其他具有ViewModelJsonModel的视图将正常呈现,只有您的ClientViewModel得到特殊处理。因此,您不会破坏应用程序的默认逻辑。

要求

  • 每个客户端有多个可能的视图
  • 未找到客户端特定视图时的默认视图回退

创建一个新服务,比如说TemplateProviderService具有简单界面的服务。

interface ViewTemplateProviderInterface
{
    public function hasTemplate($name);
    public function getTemplates();
    public function setTemplates($templates);
    public function getTemplate($name);
    public function setTemplate($name, $template);
    public function removeTemplate($name);
    public function removeTemplates();
}

控制器类中注入模板名称并对其进行硬编码。

// Some controller class
public function fooAction()
{
   $view = new ViewModel();
   $view->setTemplate($this->templateProvider->get('some_view_name'));
   return $view;
}

现在,您可以创建特定于客户端的工厂,将自定义模板脚本配置注入模板提供程序。然后,您需要做的就是确定要注入控制器的模板提供程序服务。

class ViewTemplateProviderFactory
{
    public function __invoke($sm, $name, $rname)
    {
        $config = $sm->get('config');
        if (! isset($config['view_template_providers'][$rname])) {
            throw new ServiceNotCreatedException(sprintf('No view template provider config for ''%s''.', $rname));
        }
        return new ViewTemplateProvider($config['view_template_providers'][$rname]);
    }
}

这里的关键是所有客户端的所有视图脚本都正常注册在"view_manager"键下,但控制器中模板的名称永远不会更改。

编辑

您可以只使用一个工厂并从配置中提取(请参阅上面的更改(。

return [
    'view_template_providers' => [
        'ClientOneTemplateProvider' => [
            'some_view_name' => 'name_of_script_1'
        ],
        'ClientTwoTemplateProvider' => [
            'some_view_name' => 'name_of_script_2'
        ],
        'ClientThreeTemplateProvider' => [
            'some_view_name' => 'name_of_script_3',
        ],
    ],
    'service_manager' => [
        'factories' => [
            'ClientOneTemplateProvider'   => 'ViewTemplateProviderFactory',
            'ClientTwoTemplateProvider'   => 'ViewTemplateProviderFactory',
            'ClientThreeTemplateProvider' => 'ViewTemplateProviderFactory',
        ],
    ],
    'view_manager' => [
        'template_map' => [
            'name_of_script_1' => __DIR__ . 'file/path/to/script',
            'name_of_script_2' => __DIR__ . 'file/path/to/script',
            'name_of_script_3' => __DIR__ . 'file/path/to/script',
        ],
    ],
];

似乎我解决了我的问题,但我不确定这是这样做的好方法。因此,如果有人能做得更好,我会让赏金运行以获得更好的解决方案(如果有的话(。

这是我所做的:

/**
 * Factory permettant d'établir que les vues client soient chargé si elle existent, avant les vues par défaut.
 */
class ClientStrategyFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $viewRenderer = $serviceLocator->get('ViewRenderer');
        $session = new 'Zend'Session'Container('Session');
        $clientList = $serviceLocator->get('Config')['customers_list'];
        $clientName = $this->clientMap($session->offsetGet('cod_entprim'), $clientList);
        $clientMap = new TemplateMapResolver($clientName);
        $viewRenderer->resolver()->getIterator()->insert($clientMap, 2);
        return new ClientStrategy($viewRenderer);
    }

    /**
     * permet de retourner le namespace du client selectionné avec la relation codpriml / nom de dossier
     * @param integer $codprim
     * @param array $clientList
     * @return array
     */
    public function clientMap($codprim, $clientList)
    {
        return (isset($clientList[$codprim])) ? $clientList[$codprim]: false;
    }
}

您可以看到我的自定义模板映射解析器需要一个clientName,这是用于加载自定义视图。但最重要的是:我不创建新的解析器,我只是通过这一行将我的解析器添加到列表中:

$viewRenderer->resolver()->getIterator()->insert($clientMap, 2);

第二个参数表示,此解析器是最高优先级(默认优先级为 1(

我的TemplateMapResolver非常简单,最重要的是:

 public function __construct($client)
    {
        $this->setClientName($client);
        if ($this->getCLientName()) {
            $map = $this->getMap();
        } else {
            $map = array();
        }
        parent::__construct($map);
    }
    /**
     * Return all custom views for one client
     * @param  array $map
     * @return array
     */
    public function getMap()
    {
        $name = $this->getClientName() . '''' . $this->getClientName() ;
        $class = new $name;
        return $class->customViews();
    }

我的解决方案是,强制我在客户端文件夹中创建一个与文件夹名称相同的类,因此,如果我的客户端名称是 TrumanShow,我将有一个这样的架构:

- [clients]
-- [TrumanShow]
--- TrumanShow.php
--- [Cart]
---- [view]
----- [cart]
------ [index]
------- cart-view.phtml
--- [Invoice]
--- [Reporting]

在这个文件中,我将拥有这个函数来声明我所有的自定义视图:

/**
     * Ici nous mettons nos custom views afin de les charger dans le template Map
     * @return array
     */
    public function customViews()
    {
        return array(
            'cartView' => __DIR__ . '/Cart/view/cart/index/cart-view.phtml',
        );
    }

因此,可以在不中断template_path_stack或我的其他路线的情况下做到这一点。现在我必须在控制器中调用setTemplate方法,如下所示:

// code ...
public function cartAction() {
    $view->setTemplate('cartView');
    return $view;
}

ZendFramework 将首先检查我的客户端文件夹中是否存在自定义视图,或者如果未找到视图,则加载公共视图。

感谢@Wilt和@AlexP的贡献和帮助。

不要把事情复杂化。 只需在渲染之前设置 ViewModel 的模板即可。

$vm = new ViewModel();
$vm->setTemplate( $user_service->getTemplate( $this->getRequest() ) );
return $vm;

如果您将用户注入这个虚构的用户服务中,并使用它来确定要注入的模板,则非常干净。

$user_service 的关注点应该与控制器操作的关注点完全不同。