在我的项目(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
中找到正确的模板。
更多细节可以在网上找到,有例子。例如,在此处检查。
优点是其他具有ViewModel
或JsonModel
的视图将正常呈现,只有您的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 的关注点应该与控制器操作的关注点完全不同。