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

Loading clients views before default views if exists

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

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

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

-   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),我尝试实现这个解决方案:

所以我有一个ClientStrategy;它的工厂是这样的:

<?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();
    }
}

因此,当涉及到我的 TemplateMapResolver 来完成他的工作时,我会这样做:

<?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;
    }
}

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

  • My template_path_stack not works anymore, so a lot of my views are broken.
  • I think this is a complete mess, to do this, that way.
  • Hard to maintain.
  • I understand a bit better, how it works, but i'm still unable to implements it the good way.

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

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

如果您将用户注入到这个虚构的用户服务中,并使用它来确定要注入哪个模板,那就很干净了。

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

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

自定义 class:

<?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.

更新

如果您想将其提升到一个新的水平,那么您可以做一个自定义视图模型,例如 ClientViewModel 扩展正常的 ViewModel class。
ClientViewModel 的构造函数同时采用客户端和模板名称:

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

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

下一步是创建一个 Application\View\ClientStrategy

此策略与渲染事件相关联,在此策略中,您添加一个 ViewRenderer 实例和您的自定义 TemplateMapResolver 集。在呈现期间,您可以从 ViewModel 获取客户端并使用此客户端在 TemplateMapResolver 中找到正确的模板。

更多的细节可以在网上找到,有例子。例如检查 here.

优点是其他带有 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' 键下注册,但是 controller 中的模板名称永远不会改变.

编辑

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

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;
    }
}

您可以看到我的自定义TemplateMapResolver 需要一个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();
    }

我的解决方案,强制我在我的客户文件夹中创建一个与文件夹同名的 class,因此,如果我的 clientName 是 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 的贡献和帮助。