在 PHP 中模拟 .NET 的 ScriptService





5.00/5 (17投票s)
一个易于使用的类,允许我们创建 PHP 类,其方法会自动公开在客户端 JavaScript 中。
引言
您是否曾希望能够简单地在 PHP 中定义一个类,然后通过某种神奇的过程将其转换为客户端 JavaScript 中的异步函数调用?Microsoft 在 ASP.NET 中通过 Web 服务的 ScriptService 和 ScriptMethod 属性实现了这一点,但 PHP 似乎缺乏类似的功能。
直到今天。
欢迎使用这个看似微小、实则复杂、易于使用的 DigifiScriptService 类。
背景
在过去的 8 个月里,我的本职工作涉及使用 ASP.NET 中的 ScriptServices 和 ScriptMethods。我发现它们非常方便,因为从动态生成的 JavaScript 调用 ScriptMethods 的便捷性使我无需任何回发即可快速构建外观精美的 Web 应用程序。
然而,在我的本职工作之余,我经营的副业几乎完全是 LAMP 技术栈,PHP 是我的首选语言。当我意识到 ScriptMethods 在 ASP.NET 中构建轻量级网页的强大功能时,我渴望在 PHP 中获得相同的功能。这归结为一个简单的愿望:我想用 PHP 编写一个类,并让它像 ASP.NET 的 ScriptServices 一样,神奇地在客户端 JavaScript 中可用。在我看来,理想的解决方案应该是只需要最少的配置或对现有页面结构进行最小的更改。
网上搜索结果寥寥无几,我找到的最好的一个需要将远程方法代码嵌入到为用户服务的页面所在的同一个文件中,并且需要程序员进行大量的初始化。它还使用了一个隐藏的 <IFRAME> 来执行发布。这根本不够理想。
在审查了我能找到的所有信息后,我决定我理想的解决方案需要满足以下要求:
- 它必须尽可能地像 ASP.NET 的 ScriptServices 和 ScriptMethods 一样工作。
- 用于调用 ScriptMethods 的客户端 JavaScript 必须是动态生成的。
- 调用 ScriptMethods 的 JavaScript 方法应该是静态的。
- 除了定义类及其方法之外,不需要进行任何初始化来生成 ScriptService 类(即,程序员不应该知道/关心它是如何工作的)。
我认为将 PHP 类转换为神奇的 JavaScript 对象将是最困难的部分,所以我从这里开始(结果证明我错了)。理想情况下,我希望简单地编写一个类,无需调用任何初始化或声明任何方法,它就能在需要时神奇地自动转换为客户端 JavaScript。我阅读了关于PHP 手册中的类的所有章节,并高兴地发现 PHP 支持反射。我现在拥有我需要的东西了。
由于 PHP 缺乏对 .NET 这样的装饰性属性的支持,我决定创建一个基类,DigifiScriptService。您所要做的就是继承这个类,任何公共函数都将自动转换为 ScriptMethods。
当然,让它做到这一点才是最有趣的部分。
第一步是覆盖构造函数,并检查请求是否包含查询字符串 "js"。这就是我决定触发 JavaScript 生成的方式。
class DigifiScriptService
{
  function __construct()
  {
    //Get the real (descendant) class' name:
    $class = get_class($this);
    
    //If they are requesting the javascript file, produce the javascript:
    if(getenv(QUERY_STRING) == 'js')
    {
      // JavaScript generation here
      // ...
    }
    
    // ... //
  }
}
get_class 函数返回正在实例化的实际类的名称。因此,如果我有一个名为 MyScriptService 的类,它继承自 DigifiScriptService 并创建了它的一个实例,get_class 将返回 "MyScriptService"。利用类名,我可以使用 PHP 中的 ReflectionClass 类来提取类中所有方法的列表。
$classBody = '';
$r = new ReflectionClass($class);
//Build the javascript methods that map to the PHP ones:
foreach($r->getMethods()as $m => $method)
{
现在,我的规则是只有公共函数才能在生成的 JavaScript 代理类中公开,所以我需要过滤掉它们(以及构造函数,因为它也是一个公共方法)。
  if($method->isPublic() && $method->name != '__construct')
  {
    $args = '';
    $argSetter = '';
    
    foreach($method->getParameters() as $i => $parameter)
    {            
      $args .= $parameter->name . ',';
      $argSetter .= "__drm_args[$i]=" . $parameter->name . ';';
    }
    
    echo 'function ' . $class . "_" . $method->name . 
         "(${args}onSuccess, onFailure, context)" . "{var __drm_args=[];
    ${argSetter}__digifiss__remoteMethodCall('" . $_SERVER['SCRIPT_NAME']. 
         "', '$method->name',__drm_args,onSuccess,onFailure,context);};";
    
    //Class body contains what appear to be redundant function definitions,
    //but this is to simulate the "static" method interface that .NET 
    //uses; now we don't have to instantiate anything in JavaScript to
    //call our functions:
    if($classBody != '') $classBody .= ',';
    $classBody .= $method->name . 
                  ":function(${args}onSuccess, onFailure, context) {" . 
                  $class . "_". $method->name . 
                  "(${args}onSuccess, onFailure, context)" . ";}";
  }
}
//Dump out the actual javascript fake class definition:
echo "var $class = { $classBody }";
最终结果?一个 JavaScript "类",您的方法可以以看似静态的方式调用。
<script type="text/javascript"> MyScriptService.MyScriptMethod(param, onSuccess, onFailure, 'some contextual data'); </script>
现在,我们如何将 JavaScript 代理类放入我们的页面?只需在您的页面中添加以下调用(最好放在 <HEAD></HEAD> 标签之间,尽管它也可以放在页面正文的几乎任何地方):
<head>
  <title>DigifiScriptService Sample Page</title>
  <? DigifiScriptService::add_service('MyScriptService.php'); ?>
</head>
那个调用做了什么?嗯,DigifiScriptService 上的静态方法就是这个:
public static function add_service($path)
{
  echo '<script type="text/javascript" src="' . 
       $path . '?js"></script>';
}
它添加了一个指向我们的 ScriptService 文件的 script 标签,并在查询字符串中传递了那个神奇的 ?js,从而返回了 JavaScript 代理。对于 ASP.NET 开发者来说,这类似于在 ScriptManager 对象中添加一个 <ScriptService> 标签。
如果您检查 JavaScript 代理类,您会注意到每个 ScriptMethod 都只是调用同一个函数:__digifiss__remoteMethodCall。这个方法位于 digifiss.js 中,是您在使用此代码时需要显式添加的唯一 JavaScript 文件。我不会在这里深入探讨它的细节(我花了很多时间访问各种网站才把它组合起来),但这是概述:
- 它只允许配置的最大并发请求数。
- 它将每个方法调用构建成一个对包含 ScriptService 的 PHP 文件的请求,使用 XMLHttpRequest对象。
- 它会自动接收调用的结果,并将结果适当地传递给 onSuccess或onFailure参数中指定的函数。
Using the Code
您的 Web 服务器上只需要 digifiss.php 和 digifiss.js 文件。其余的由您自己编写。
定义您的 ScriptService
<?
// MyScriptService.php
include_once 'digifiss.php';
class MyScriptService extends DigifiScriptService
{
  //This is a public function which will be turned into a ScriptMethod
  public function HelloWorld()
  {
    return "Hello World";
  }
  
  //A ScriptMethod with arguments:
  public function Add($x, $y)
  {
    return $x + $y;
  }
  
  //A private method, will be ignored by the JavaScript generator:
  private function DoSomethingPrivate()
  {
    return "Not Exposed";
  }
}
//This is the only line of initialization required:
new MyScriptService();
?>
使用您的 ScriptService
在您的实际页面中,只有一点点开销;一个 include_once,一个对 DigifiScriptService 的静态方法的调用,以及对 digifiss.js 的一个引用。
<?
//SamplePage.php
include_once 'digifiss.php';
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
  <meta http-equiv="content-type" 
         content="text/html; charset=windows-1250">
  <title>DigifiScriptService Sample Page</title>
  <script type="text/javascript" src="digifiss.js"></script>
  <? DigifiScriptService::add_service('MyScriptService.php'); ?>
  </head>
  <body>
  
  <a href="javascript:DoHelloWorld();">Click for Hello World</a>
  <div id="HelloWorld"></div>
  <br />
  <br />
  <a href="javascript:DoAddition();">Add 2 + 2</a>
  <div id="Addition"></div>
  <br />
  <br />
  
  <script type="text/javascript">
  function DoHelloWorld()
  {
    document.getElementById('HelloWorld').innerHTML = 'Loading...';
    MyScriptService.HelloWorld(onSuccess, onFailure, 'HelloWorld');
  }
  
  function DoAddition()
  {
    document.getElementById('Addition').innerHTML = 'Loading...';
    MyScriptService.Add(2, 2, onSuccess, onFailure, 'Addition');
  }
  
  function onSuccess(result, context, method)
  {
    document.getElementById(context).innerHTML = result;
  }
  
  function onFailure(result, context, method)
  {
    document.getElementById(context).innerHTML = result;
  }
  </script>
  </body>
</html>
附加代码
我在本职工作中进行的许多 ScriptMethod 调用都涉及连接到数据库,将数据提取为 XML,然后对其应用 XSL 转换,并将生成的 HTML 返回给客户端,以便插入到某个 <div> 元素的 innerHTML 值中。为了在 PHP 中提供此功能,我在 DigifiScriptService 类中添加了两个附加函数,您可以自由使用它们:
- mysql_query_to_xml- 接受一个 MySQL 连接链接 ID、要运行的 SQL 以及父节点和子节点的名称,并返回一个 XML- DOMDocument对象,该对象包含您 SQL 结果的每一行,作为父节点的子节点。
- mysql_query_to_xsl- 接受一个 MySQL 连接链接 ID、要运行的 SQL、父节点和子节点的名称以及 XSL 样式表文件的路径,并返回将 MySQL 结果转换为 XML 并使用 XSL 文件进行转换的结果。
有关如何使用这些功能的示例,请参阅以下示例:
<?
// DatabaseExample.php
include_once 'digifiss.php';
class DatabaseExample extends DigifiScriptService
{
  public function FindCustomers($searchString)
  {
    $connection = mysql_connect("localhost", "dbuser", "password");
    @mysql_select_db("customers", $connection);
    
    $sql = "SELECT customerId, firstName, lastName FROM customers" . 
           " WHERE CONCAT(firstName, ' ', lastName) " . 
           "LIKE '%$searchString%' ORDER BY lastName";
    
    //Produces an HTML <table> containing the search results:
    $result = $this->mysql_query_to_xsl($connection, $sql, 'Customers', 
                                           'Customer', 'searchresults.xsl');
    mysql_close($connection);
    return $result;
  }
}
//This is the only line of initialization required:
new DatabaseExample();
?>
重要说明
- 目前,您无法通过此方法上传文件。由于 JavaScript 安全性的工作方式,这种情况不太可能改变。
- 我在 Internet Explorer 7 和 FireFox 3.0 上对此进行了测试。我假设它在 IE 6 中也能工作,但我没有副本。我不知道它是否能在 Safari 或任何其他平台上工作。
- 对于那些对内部机制感兴趣的人,我尽量添加了注释。
关注点
我从午夜开始工作,直到凌晨 4:30,第一个版本使用 <IFRAME> 进行数据发布,效果很好。
当我 6 小时后醒来时,我决定 IE 在发布到 IFrame 时发出的“咔哒”声是不可接受的,并又花了几个小时研究 XMLHttpRequest 解决方案。
历史
- 2009/02/21 - 初始版本。


