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

时间:2015-12-11 13:09:22

标签: php zend-framework2

在我的项目(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;
    }
}

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

  
      
  • 我的template_path_stack不再有用了,所以我的很多观点都被打破了。
  •   
  • 我认为这是一个完全混乱,这样做。
  •   
  • 难以维持。
  •   
  • 我理解得更好,它是如何运作的,但我仍然无法以良好的方式实现它。
  •   

4 个答案:

答案 0 :(得分:2)

如果你真的想这样做(我不确定它是否是最佳方式),那么你可以使用自定义逻辑扩展TemplateMapResolver并将其设置在Renderer实例中。< / p>

制作自定义课程:

<?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类。
ClientViewModel的构造函数同时包含客户端和模板名称:

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

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

下一步是创建Application\View\ClientStrategy

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

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

优势在于ViewModelJsonModel的其他观看次数将正常呈现,只有您的ClientViewModel获得特殊待遇。因此,您不会破坏应用程序的默认逻辑。

答案 1 :(得分:2)

要求

  • 每个客户多个可能的视图
  • 如果找不到客户特定视图,则默认视图回退

创建一个新服务,比如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',
        ],
    ],
];

答案 2 :(得分:1)

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

以下是我所做的:

/**
 * 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,这是用于加载自定义视图。但最重要的是:我没有创建一个新的Resolver,我只是通过这一行将我的Resolver添加到列表中:

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

我的解决方案,强迫我在我的客户端文件夹中创建一个具有相同文件夹名称的类,因此,如果我的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的贡献和帮助。

答案 3 :(得分:-1)

不要过度复杂化。只需在渲染之前设置ViewModel的模板。

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

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

$ user_service的关注应该与您对Controller操作的关注完全不同。