valu/valuso

此包已被放弃,不再维护。未建议替代包。

面向服务的应用架构的Zend Framework 2模块

v1.0.2 2015-03-12 20:09 UTC

This package is auto-updated.

Last update: 2020-10-08 16:01:31 UTC


README

Build Status

ValuSo是一个Zend Framework 2模块,用于面向服务的应用架构。

安装

使用 Composer 安装包 valu/valuso

php composer.phar require valu/valuso

当询问安装哪个版本时,输入 dev-master。现在它已准备好作为库使用。要将其作为ZF2模块使用,请将 valuso 添加到 config/application.config.php 中的模块列表中。

服务层

通过服务层定义应用边界,该层通过一组可用的操作并协调应用在每个操作中的响应来建立应用边界。 - Randy Stafford, EAA

在ValuSo中,服务层是通过使用 service broker 实现的,它将操作传递到正确的服务实现并收集它们的响应。当调用操作时,服务代理会调用已注册的 closure__invoke 方法。如果没有 __invoke 可用,服务类将用特殊的 proxy class 包装,该类通过将操作名称映射到方法名称实现 __invoke 方法。

服务的消费者不知道谁实际上实现了服务以及在哪里(本地或外部系统)。

服务层的概念和实现类似于MVC模式中的路由器/控制器交互。然而,它们在不同的级别上操作。控制器是接收用户(或客户端)原始请求的端点(例如,来自HTTP)。控制器需要决定谁负责处理请求。有了服务层,控制器调用适当的服务,并返回可能实际上是来自多个服务的响应的聚合响应。

无MVC或MVSC

使用ValuSo,开发人员可以选择忽略常见的MVC模式或通过服务层扩展它。ValuSo提供了三个预配置的路由,用于 HTTP RESTHTTP RPCconsole 接口。这些路由指向 ServiceController,它提供两个动作

  • httpAction 用于HTTP REST/RPC请求和
  • consoleAction 用于控制台请求。

这些动作能够将客户端请求转换为服务调用,并将服务响应转换为正确的HTTP/CLI响应格式(在两种情况下都是JSON)。

ValuSo旨在与需要后端与前端完全分离的应用程序一起使用。因此,MVC模式中的 View 概念通常是不明确的。

特性

便捷且IDE安全的服务使用方式

通常,通过调用ServiceBroker类的service()方法来访问服务。此方法初始化一个新的Worker对象。要跳过此初始化,可以直接调用ServiceBroker的execute()dispatch()方法。

// Fetch broker from main service locator
$serviceBroker = $serviceLocator->get('ServiceBroker');

/** 
 * Initialize Worker, but tell the IDE to treat $service
 * as an instance of UserService class
 * @var $service \ValuUser\Service\UserService 
 */
$service = $serviceBroker->service('User');

// Call 'create' operation
$service->create('administrator');

通过HTTP调用服务

ValuSo为HTTP REST和RPC接口提供了端点(控制器)。这两个接口之间的区别很小,你应该坚持使用其中一个。这两个接口都返回JSON格式的响应。

使用REST接口锁定用户账户

POST /rest/user/029713b396493b01bfc619a7493e2cba
X-VALU-OPERATION: lock

响应

{"d":true}

使用RPC接口查找一个组

GET /rpc/group/find/029713b396493b01bfc619a7493e2cba

响应

{"d":{"name":"sales-people", "createdAt":"2013-01-28T10:13:21+0200", "updatedAt":...}}

执行批量操作

批量操作对于性能来说非常重要,尤其是在通过外部接口调用服务时。

POST /rest/batch
q: {
	"commands":{
		"cmd1": {"service":"user", "operation":"remove", "query":"$administrator"},
		"cmd1": {"service":"group", "operation":"remove", "query":"/administrators"}
	}
}

响应

{
	"d": {
		"cmd1": true,
		"cmd2": true
	}
}

轻松使用现有类作为服务

ServiceBroker期望作为服务注册的类提供__invoke()方法。在大多数情况下,类没有实现此方法,这表示ServiceBroker(确切地说是ServicePluginManager)应该将这些服务包装在特殊的代理类中。通过此功能,可以使用几乎任何现有的类作为服务。

// Assume that service with ID 'ZendLogger' is registered
// to ServiceManager
$serviceBroker
	->getLoader()
	->register('ZendLogger', 'Log');

$serviceBroker->service('Log')->err('Something went wrong');

上下文感知服务

通常,服务需要知道它们在哪个上下文中执行。大多数情况下,这是因为某些服务不应公开给外部接口。以下示例通过在不支持上下文中调用update操作来演示这一点。

$serviceBroker
	->service('User')
	->context('http-get')
	->update(['name' => 'Mr Smith']);
// Throws UnsupportedContextException

使用注解服务的面向方面编程

服务有许多横切关注点。大多数服务需要触发事件、提供日志记录和访问控制机制。在不干扰实际业务代码的情况下实现这些功能的最佳方式是注解操作。

use ValuSo\Annotation as ValuService;
class UserService {

    /**
     * Create user
     * 
     * @ValuService\Context("http-put")
     * @ValuService\Log({"args":"username","level":"info"})
     * @ValuService\Trigger("pre")
     * @ValuService\Trigger("post")
     */
    public function create($username, array $specs)
    {
        // create a new user
    }
}

一个服务有多个响应者

通常,每个服务只有一个类或闭包,但架构并不局限于这一点。实际上,可以为同一个服务名称注册任意数量的类/闭包。

$loader = $serviceBroker->getLoader();
$loader->registerService('HmacAuth', 'Auth', new HmacAuthenticationService());
$loader->registerService('HttpBasicAuth', 'Auth', new HttpBasicAuthenticationService());
$loader->registerService('HttpDigestAuth', 'Auth', new HttpDigestAuthenticationService());

// Authenticate until one of the respondents returns
// either boolean true or false and retrieve that value
$isAuthenticated = $serviceBroker
	->service('Auth')
	->until(function($response) {return is_bool($response);})
	->authenticate($httpRequest)
	->last();

使用您的实现扩展任何现有服务

通常,如果服务实现不支持某些操作,则抛出UnsupportedOperationException。CommandManager不会在此处停止执行,而是找到下一个注册的类/闭包为该服务,并给它执行该操作的机会。可以使用此功能扩展现有服务。

class ExtendedUserService {
    public function findExpiredAccounts() {
        // run some special find operation
    }    
}

$loader = $serviceBroker->getLoader();
$loader->registerService('ExtendedUserService', 'User', new ExtendedUserService());

$serviceBroker->service('User')->findExpiredAccounts();

监听服务

监听服务操作通常很重要。ServiceBroker提供EventManager的实例,并在操作被调用前后自动触发事件。EventManagerAware服务类也可以触发自己的事件。

$serviceBroker
	->getEventManager()
	->attach(
		'post.valuuser.remove',
		function($e) use($serviceBroker) {
            $user = $e->getParam('user');
            
            if ($user->getUsername() === 'administrator') {
                $serviceBroker->service('Group')->remove('/administrators');
            }
		}
	);

在服务内调用服务

在服务类内部使用其他服务是常见的。为了方便,ValuSo提供了ServiceBrokerAware接口。此接口用于注入ServiceBroker实例。还有一个内置的特性,即ServiceBrokerTrait,它会为您实现该接口。

class UserModuleSetupService 
	implements ServiceBrokerAwareInterface
{
	use ServiceBrokerTrait;

	public function setup()
	{
		// Create administrator
		$this->getServiceBroker()
			->service('User')
			->create('administrator');

		return true;
	}
}

使用proxy()方法调用内部方法

当使用服务代理类时,代理类会实现缺失的__invoke方法,将操作名称映射到内部方法名称并调用该方法。代理类实际上覆盖了默认方法。典型的代理方法实现如下

public function create($name, array $specs = array())
{
    $response = $this->__wrappedObject->create($name, $specs);

    $__event_params = new \ArrayObject();
    $__event_params["name"] = $name;
    $__event_params["specs"] = $specs;
    $__event_params["__response"] = $response;
    // Trigger "post" event
    if (sizeof($this->__commandStack)) {
        $__event = new \ValuSo\Broker\ServiceEvent('post.valuaccount.create', $this->__wrappedObject, $__event_params);
        $__event->setCommand($this->__commandStack[sizeof($this->__commandStack)-1]);
        $this->getEventManager()->trigger($__event);
    }
    return $response;
}

如您所见,第一行调用实际的方法实现,以下代码行定义事件参数,最后触发一个'post'事件。

您无法从实际类中正常调用这些代理方法。然而,通过特殊的proxy()方法可以做到。这是因为,如果没有使用proxy()方法,将无法调用另一个方法并触发其事件等。

如果代理实例不可用,proxy()方法返回对$this的引用。这种模式确保了即使没有代理实例,服务也可以进行测试和使用。

class UserService
{
    public function update($query, array $specs)
    {
        $user = $this->resolveUser($query);
        return $this->proxy()->doUpdate($user, $specs);
    }


    /**
     * @ValuService\Trigger({"type":"post","name":"post.<service>.update"})
     */
    protected function doUpdate(User $user, array $specs)
    {
        // update
    }
}

入门指南

通常每个服务对应一个类。服务类使用ZF2的ServiceManager进行初始化,这意味着您需要配置服务类名称或工厂类名称。

以下是一个来自ValuUser模块的示例。该模块注册了一个名为User的单个服务。该服务仅配置了类名。

<?php
namespace ValuUser;

use Zend\ModuleManager\Feature\ConfigProviderInterface;

class Module implements ConfigProviderInterface
{

    public function getConfig()
    {
        return [
        'valu_so' => [
            'services' => [
                'ValuUser' => [
			        		'class' => 'ValuUser\Service\UserService'
                ]
	        ]
        ]];
    }
}

通常,在没有注入一些依赖项或配置服务的情况下无法初始化服务。在这些场景中,应使用服务工厂来初始化服务(请参阅Zend Framework的文档以了解如何使用ServiceManager)。

return [
'valu_so' => [
    'services' => [
        'ValuUser' => [
	        'factory' => 'ValuUser\Service\UserServiceFactory'
        ]
    ]
]];

还可以注册可调用类的实例或闭包。出于性能考虑,这通常不推荐。服务应在需要时才进行初始化。

return [
'valu_so' => [
    'services' => [
        'ValuUser' => [
	        'service' => new ValuUserService()
        ]
    ]
]];

配置选项

ValuSo配置是从valu_so配置命名空间中读取的。

$config = [
  'valu_so' => [
      // Set true to add main service locator as a peering service manager
      'use_main_locator'   => <true>|<false>, 
      // See Zend\Mvc\Service\ServiceManagerConfig
      'factories'          => [...],
      // See Zend\Mvc\Service\ServiceManagerConfig 
      'invokables'         => [...],
      // See Zend\Mvc\Service\ServiceManagerConfig 
      'abstract_factories' => [...],
      // See Zend\Mvc\Service\ServiceManagerConfig
      'shared'             => [...],
      // See Zend\Mvc\Service\ServiceManagerConfig
      'aliases'            => [...],
      'cache'              => [
          'enabled' => true|false, 
          'adapter' => '<ZendCacheAdapter>', 
          'service' => '<ServiceNameReturningCacheAdapter', 
          <adapterConfig> => <value>...
      ],
      'services' => [
          '<id>' => [
              // Name of the service
              'name'     => '<ServiceName>',
              // [optional] Options passed to service when initialized
              'options'  => [...],
              // [optional] Service class (same as defining it in 'invokables')
              'class'    => '<Class>',
              // [optional] Factory class  (same as  defining it in 'factories')
              'factory'  => '<Class>',
              // [optional] Service object/closure
              'service'  => <Object|Closure>,
              // [optinal] Priority number, defaults to 1, highest number is executed first 
              'priority' => <Priority> 
          ]
      ]
  ]
],