Skip to content

5. XML 和 PHP

在本章中,我们将介绍如何在 PHP 中使用 XML(eXtensible 标记语言)文档。我们将以上一章所探讨的税务应用程序为例进行说明。

5.1. XML 文件和 XSL 样式表

请看以下 XML 文件,它可能代表模拟结果:

<?xml version="1.0" encoding="windows-1252"?>
<simulations>
  <simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>
  <simulation marie="non" enfants="2" salaire="200000" impot="33388"/>
</simulations>

如果使用 IE 6 进行查看,将得到以下结果:

Image

IE6 通过文件的 .xml 后缀识别出这是 XML 文件,并以自身特有的方式进行排版。 使用 Netscape 则会显示一个空白页面。然而,如果查看源代码(View/Source),确实可以看到原始的 XML 文件:

Image

为什么 Netscape 没有显示任何内容?因为它需要一个样式表来告诉它如何将 XML 文件转换为 HTML 文件,这样它才能显示出来。 恰巧 IE 6 本身带有默认样式表,而 XML 则没有提供样式表,这正是此处的情况。

有一种名为 XSL(eXtensible StyleSheet 语言)的语言,用于描述将 XML 文件转换为任意文本文件所需的转换过程。 XSL支持多种指令,其语法与编程语言极为相似。本文将不作详细阐述,因为这需要数十页的篇幅。 我们仅列举两个 XSL 样式表的示例。第一个样式表用于将 XML 文件转换为 simulations.xml 代码。 我们修改该文件,使其指定浏览器可使用的样式表,从而将其转换为浏览器可显示的 HTML 文档:

<?xml version="1.0" encoding="windows-1252"?>
<?xml-stylesheet type="text/xsl" href="simulations.xsl"?>
<simulations>
  <simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>
  <simulation marie="non" enfants="2" salaire="200000" impot="33388"/>
</simulations>

命令 XML

<?xml-stylesheet type="text/xsl" href="simulations.xsl"?>

将文件 simulations.xsl 指定为类型为 text/xsl 的样式表(xml-stylesheet)c.a.d。一个包含代码的文本文件 XSL。 浏览器将使用此样式表将文本 XML 转换为文档 HTML。以下是在 Netscape 7 中加载文件 XML simulations.xml 时获得的结果:

Image

当我们查看文档的源代码(View/Source)时,发现的是原始文档 XML,而非显示的 HTML:

Image

Netscape 使用样式表 simulations.xsl 将上述文档 XML 转换为可显示的文档 HTML。现在是时候查看该样式表的内容了:

<?xml version="1.0" encoding="windows-1252"?>
  <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
      <xsl:output method="html" indent="yes"/>

    <xsl:template match="/">
        <html>
          <head>
            <title>Simulations de calculs d'impôts</title>
        </head>
          <body>
            <center>
              <h3>Simulations de calculs d'impôts</h3>
            <hr/>
            <table border="1">
                    <th>marié</th><th>enfants</th><th>salaire</th><th>impôt</th>
                          <xsl:apply-templates select="/simulations/simulation"/>
                        </table>
         </center>
       </body>
     </html>
        </xsl:template>


    <xsl:template match="simulation">
          <tr>
            <td><xsl:value-of select="@marie"/></td>
            <td><xsl:value-of select="@enfants"/></td>
            <td><xsl:value-of select="@salaire"/></td>
            <td><xsl:value-of select="@impot"/></td>
        </tr>
       </xsl:template>
  </xsl:stylesheet>
  • 样式表 XSL 是一个 XML 文件,因此遵循其规则。它必须“格式正确”,即所有打开的标签都必须关闭。
  • 该文件以两条命令开头,在 Windows 系统中的任何样式表中均可保留:
<?xml version="1.0" encoding="windows-1252"?>
  <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

encoding="windows-1252" 属性允许在样式表中使用带重音的字符。

  • 标签 <xsl:output method="html" indent="yes"/> 指示解析器 XSL 生成“缩进”的 HTML 内容。
  • 标签 <xsl:template match="元素"> 用于定义 XML 文档中将应用 <xsl:template ...> </xsl:template> 之间指令的元素。
    <xsl:template match="/">
................
        </xsl:template>

在上例中,元素“/”表示文档的根节点。这意味着一旦遇到文档 XML 的开头,位于两个标签之间的 XSL 命令就会被执行。

  • 除 XSL 标签外,其余内容将原样写入输出流。XSL 标签会被执行。其中部分标签会在输出流中产生结果。让我们来看以下示例:
    <xsl:template match="/">
        <html>
          <head>
            <title>Simulations de calculs d'impôts</title>
        </head>
          <body>
            <center>
              <h3>Simulations de calculs d'impôts</h3>
            <hr/>
            <table border="1">
                    <th>marié</th><th>enfants</th><th>salaire</th><th>impôt</th>
                          <xsl:apply-templates select="/simulations/simulation"/>
                        </table>
          </center>
        </body>
      </html>
        </xsl:template>

需要说明的是,所分析的文档XML如下:

<?xml version="1.0" encoding="windows-1252"?>
<simulations>
  <simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>
  <simulation marie="non" enfants="2" salaire="200000" impot="33388"/>
</simulations>

从所分析的文档 XML 的开头(match="/") 开始,解析器 XSL 将输出文本

<html>
          <head>
            <title>Simulations de calculs d'impôts</title>
        </head>
          <body>
            <center>
              <h3>Simulations de calculs d'impôts</h3>
            <hr>
            <table border="1">
                    <th>marié</th><th>enfants</th><th>salaire</th><th>impôt</th>

需要注意的是,在原始文本中我们使用的是 <hr/>,而不是 <hr>。在原始文本中无法写成 <hr>,因为虽然它是有效的 HTML 标签,但却是无效的 XML 标签。 然而,我们这里处理的是一个必须“格式正确”的文本,即所有标签都必须闭合。 因此我们写成 <hr/>,并且因为我们写了 <xsl:output text="html ...">,解释器会将文本 <hr/> 转换为 <hr>。紧随其后的是由命令 XSL 生成的文本:

      <xsl:apply-templates select="/simulations/simulation"/>

我们稍后将了解该文本的具体内容。最后,解释器会添加以下文本:

                    </table>
          </center>
        </body>
      </html>

<xsl:apply-templates select="/simulations/simulation"/> 指令要求执行元素 /simulations/simulation 的“模板”。 每当解析器 XSL 在已解析的文本 XML 中遇到位于 <simulations>..</simulations> 标签内的 <simulation>..</simulations> <simulation/> 标签时,该指令就会被执行。 遇到此类标签时,解释器将执行以下模板中的指令:

    <xsl:template match="simulation">
          <tr>
            <td><xsl:value-of select="@marie"/></td>
            <td><xsl:value-of select="@enfants"/></td>
            <td><xsl:value-of select="@salaire"/></td>
            <td><xsl:value-of select="@impot"/></td>
        </tr>
       </xsl:template>

我们来看以下 XML 行:

<simulations>
  <simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>

<simulation ..> 对应于指令 XSL 的模板 <xsl:apply-templates select="/simulations/simulation">。因此,解释器 XSL 将尝试应用与该模板对应的指令。 它将找到模板 <xsl:template match="simulation"> 并执行它。 需要提醒的是,非 XSL 命令的内容会被 XSL 解释器原样保留,而 XSL 命令则会被其执行结果所替换。 因此,指令 XSL <xsl:value-of select="@champ"/> 会被替换为所解析节点(此处为 <simulation> 节点)的 "champ" 属性的值。对前一行 XML 的解析将产生以下输出结果:

XSL
输出
<tr><td>
<tr><td>
<xsl:value-of select="@marie"/>
</td><td>
</td><td>
<xsl:value-of select="@children"/>
2
</td><td>
</td><td>
<xsl:value-of select="@salaire"/>
200000
</td><td>
</td><td>
<xsl:value-of select="@impot"/>
22504
</td></tr>
</td></tr>

总计,行 XML

<simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>

将转换为行 HTML:

<tr><td>oui</td><td>2</td><td>200000</td><td>22504</td></tr>

虽然这些解释略显简略,但读者现在应该能够看出,以下文本 XML:

<?xml version="1.0" encoding="windows-1252"?>
<?xml-stylesheet type="text/xsl" href="simulations.xsl"?>
<simulations>
  <simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>
  <simulation marie="non" enfants="2" salaire="200000" impot="33388"/>
</simulations>

附带以下样式表 XSL simulations.xsl

<?xml version="1.0" encoding="windows-1252"?>
  <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
      <xsl:output method="html" indent="yes"/>

    <xsl:template match="/">
        <html>
          <head>
            <title>Simulations de calculs d'impôts</title>
        </head>
          <body>
            <center>
              <h3>Simulations de calculs d'impôts</h3>
            <hr/>
            <table border="1">
                    <th>marié</th><th>enfants</th><th>salaire</th><th>impôt</th>
                          <xsl:apply-templates select="/simulations/simulation"/>
                        </table>
         </center>
       </body>
     </html>
        </xsl:template>


    <xsl:template match="simulation">
          <tr>
            <td><xsl:value-of select="@marie"/></td>
            <td><xsl:value-of select="@enfants"/></td>
            <td><xsl:value-of select="@salaire"/></td>
            <td><xsl:value-of select="@impot"/></td>
        </tr>
       </xsl:template>
  </xsl:stylesheet>

生成以下文本 HTML:

<html>
<head>
<title>Simulations de calculs d'impôts</title>
</head>
<body>
<center>
<h3>Simulations de calculs d'impots</h3>
<hr>
<table border="1">
<th>marié</th><th>enfants</th><th>salaire</th><th>impôt</th>
<tr>
<td>oui</td><td>2</td><td>200000</td><td>22504</td>
</tr>
<tr>
<td>non</td><td>2</td><td>200000</td><td>33388</td>
</tr>
</table>
</center>
</body>
</html>

文件 XML simulations.xml 接下来

<?xml version="1.0" encoding="windows-1252"?>
<?xml-stylesheet type="text/xsl" href="simulations.xsl"?>
<simulations>
  <simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>
  <simulation marie="non" enfants="2" salaire="200000" impot="33388"/>
</simulations>

在最新浏览器(此处为 Netscape 7)中显示如下:

Image

5.2. 税务应用程序:第 5 版

5.2.1. 税务应用程序中的文件 XML 和样式表 XSL

让我们回到最初的起点——税务Web应用程序,并回顾一下我们的目标:希望将其修改为向客户端返回XML格式的响应,而非HTML。 该 HTML 响应将附带一个 XSL 样式表,以便浏览器能够正确显示。在上段中,我们介绍了:

  • 文件 simulations.xml,它是响应 XML 的原型,其中包含税款计算的模拟
  • 文件 simulations.xsl,它将作为样式表 XSL,随附于响应文件 XML

我们还需考虑包含错误的响应情况。在此情况下,响应 XML 的原型文件将是以下 erreurs.xml 文件:

<?xml version="1.0" encoding="windows-1252"?>
<?xml-stylesheet type="text/xsl" href="erreurs.xsl"?>
<erreurs>
    <erreur>erreur 1</erreur>
  <erreur>erreur 2</erreur>
</erreurs>

用于在浏览器中显示此文档 XML 的样式表 erreurs.xsl 如下:

<?xml version="1.0" encoding="windows-1252"?>
  <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
      <xsl:output method="html" indent="yes"/>

    <xsl:template match="/">
        <html>
          <head>
            <title>Simulations de calculs d'impôts</title>
        </head>
          <body>
            <center>
              <h3>Simulations de calculs d'impôts</h3>
          </center>            
          <hr/>
          Les erreurs suivantes se sont produites :
          <ul>
                  <xsl:apply-templates select="/erreurs/erreur"/>
              </ul>            
        </body>
      </html>
        </xsl:template>

    <xsl:template match="erreur">
          <li><xsl:value-of select="."/></li>
        </xsl:template>
</xsl:stylesheet>    

该样式表引入了一个尚未出现的 XSL 指令:<xsl:value-of select="."/>。该指令的输出结果为所解析节点的值,此处为 <erreur>texte</erreur> 节点。 该节点的值即为两个起始标签和结束标签之间的文本,此处为 texte

代码 erreurs.xml 通过样式表 erreurs.xsl 转换为以下文档 HTML:

<html>
<head>
<title>Simulations de calculs d'impots</title>
</head>
<body>
<center>
<h3>Simulations de calculs d'impots</h3>
</center>
<hr>
          Les erreurs suivantes se sont produites :
          <ul>
<li>erreur 1</li>
<li>erreur 2</li>
</ul>
</body>
</html>

文件 erreurs.xml 及其样式表在浏览器中的显示效果如下:

Image

5.2.2. xmlsimulations 应用程序

我们创建一个名为 xmlsimulations.html 的文件,并将其放置在 impots 应用程序的目录中。显示的页面如下:

Image

该 HTML 文档是一个静态文档。其代码如下:

<html>
    <head>
      <title>impots</title>
    <script language="JavaScript" type="text/javascript">
        function effacer(){
           // 表单清空
        with(document.frmImpots){
            optMarie[0].checked=false;
          optMarie[1].checked=true;
          txtEnfants.value="";
          txtSalaire.value="";
          txtImpots.value="";
        }//with
      }//清除

      function calculer(){
           // 在将参数发送至服务器前进行验证
        with(document.frmImpots){
          //子节点数量
          champs=/^\s*(\d+)\s*$/.exec(txtEnfants.value);
          if(champs==null){
            // 未验证表单
            alert("Le nombre d'enfants n'a pas été donné ou est incorrect");
            nbEnfants.focus();
            return;
          }//if
           //工资
          champs=/^\s*(\d+)\s*$/.exec(txtSalaire.value);
          if(champs==null){
            // 未验证模板
            alert("Le salaire n'a pas été donné ou est incorrect");
            salaire.focus();
            return;
          }//if
          // 没问题 - 发送
          submit();
        }//附带
      }//计算        
        </script>
  </head>

  <body background="/poly/impots/7/images/standard.jpg">
      <center>
        Calcul d'impôts
        <hr>
      <form name="frmImpots" action="/poly/impots/7/xmlsimulations.php" method="POST">
          <table>
            <tr>
              <td>Etes-vous marié(e)</td>
            <td>
                <input type="radio" name="optMarie" value="oui">oui
              <input type="radio" name="optMarie" value="non" checked>non
            </td>
          </tr>
          <tr>
              <td>Nombre d'enfants</td>
            <td><input type="text" size="3" name="txtEnfants" value=""></td>
          </tr>
          <tr>
              <td>Salaire annuel</td>
            <td><input type="text" size="10" name="txtSalaire" value=""></td>
          </tr>
          <tr></tr>
          <tr>
              <td><input type="button" value="Calculer" onclick="calculer()"></td>
            <td><input type="button" value="Effacer" onclick="effacer()"></td>
          </tr>
        </table>
      </form>
    </center>
 </body>
</html>

需要注意的是,表单数据将提交至 URL /poly/impots/7/xmlsimulations.php。 应用程序代码 xmlsimulations.php 与应用程序代码 impots.php 非常相似。建议读者重新查阅后者。在此重申输入代码:

<?php
     // 处理税务表格

     // 库
  include "ImpotsDSN.php";

   // 开始会话
  session_start();

   // 应用程序配置
  ini_set("register_globals","off");
  ini_set("display_errors","off");
  $formulaireImpots="impots_form.php";
  $erreursImpots="impots_erreurs.php";
  $bdImpots=array(dsn=>"mysql-dbimpots",user=>admimpots,pwd=>mdpimpots,
      table=>impots,limites=>limites,coeffR=>coeffR,coeffN=>coeffN);

     // 获取会话参数
  $session=$_SESSION["session"];
   // 会话有效吗?
  if(! isset($session) || ! isset($session[objImpots]) || ! isset($session[simulations])){
       // 启动新会话
    $session=array(objImpots=>new ImpotsDSN($bdImpots),simulations=>array());
    // 是否出现错误?
    if(count($session[objImpots]->erreurs)!=0){
      $requête=array(erreurs=>$session[objImpots]->erreurs);
         // 显示错误页面
        include $erreursImpots;
       // 结束
      $session=array();
      terminerSession($session);
    }//if
  }//if

  // 获取当前交换的参数
  $requête[marié]=$_POST["optMarie"];
  $requête[enfants]=$_POST["txtEnfants"];
  $requête[salaire]=$_POST["txtSalaire"];

   // 是否已获取所有参数?
  if(! isset($requête[marié]) || ! isset($requête[enfants]) || ! isset($requête[salaire])){
       // 显示空表单
    $requête=array(chkoui=>"",chknon=>"checked",enfants=>"",salaire=>"",impots=>"",
            erreurs=>array(),simulations=>array());    
    include $formulaireImpots;
     // 结束
    terminerSession($session);
  }//if

   // 验证参数
  $requête=vérifier($requête);

   // 有错误吗?
  if(count($requête[erreurs])!=0){
       // 显示表单
    include "$formulaireImpots";
     // 结束
    terminerSession($session);
  }//if

   // 应缴税款计算
  $requête[impots]=$session[objImpots]->calculer(array(marié=>$requête[marié],
        enfants=>$requête[enfants],salaire=>$requête[salaire]));

   // 另一项模拟
  $session[simulations][]=array($requête[marié],$requête[enfants],$requête[salaire],$requête[impots]);
  $requête[simulations]=$session[simulations];

   // 显示表单
  include "$formulaireImpots";

   // 结束
  terminerSession($session);
...

HTML 页面是由包含语句“... ”生成的。这里我们希望生成 XML,而不是 HTML。 只需编写两个新的应用程序 impots_erreurs.phpimpots_simulations.php,即可生成 XML 而不是 HTML。 应用程序的其余部分保持不变。代码如下:

<?php
     // 处理税务表格

     // 库
  include "ImpotsDSN.php";

   // 会话启动
  session_start();

   // 应用程序配置
  ini_set("register_globals","off");
  ini_set("display_errors","off");
  $formulaireImpots="xmlsimulations.html";
  $erreursImpots="impots_erreurs.php";
  $simulationsImpots="impots_simulations.php";
  $bdImpots=array(dsn=>"mysql-dbimpots",user=>admimpots,pwd=>mdpimpots,
      table=>impots,limites=>limites,coeffR=>coeffR,coeffN=>coeffN);

     // 获取会话参数
  $session=$_SESSION["session"];
   // 会话有效吗?
  if(! isset($session) || ! isset($session[objImpots]) || ! isset($session[simulations])){
       // 启动新会话
    $session=array(objImpots=>new ImpotsDSN($bdImpots),simulations=>array());
    // 是否出现错误?
    if(count($session[objImpots]->erreurs)!=0){
      $requête=array(erreurs=>$session[objImpots]->erreurs);
         // 以 XML 格式显示错误页面
          header("Content-type: text/xml");      
        include $erreursImpots;
       // 结束
      $session=array();
      terminerSession($session);
    }//if
  }//if

   // 获取当前交换的参数
  $requête[marié]=$_POST["optMarie"];
  $requête[enfants]=$_POST["txtEnfants"];
  $requête[salaire]=$_POST["txtSalaire"];

   // 是否已获取所有参数?
  if(! isset($requête[marié]) || ! isset($requête[enfants]) || ! isset($requête[salaire])){
       // 显示空表单
    include $formulaireImpots;
     // 结束
    terminerSession($session);
  }//if

   // 验证参数
  $requête=vérifier($requête);

   // 有错误吗?
  if(count($requête[erreurs])!=0){
       // 以 XML 格式显示错误
      header("Content-type: text/xml");    
    include "$erreursImpots";
     // 结束
    terminerSession($session);
  }//if

   // 应缴税款计算
  $requête[impots]=$session[objImpots]->calculer(array(marié=>$requête[marié],
        enfants=>$requête[enfants],salaire=>$requête[salaire]));

   // 另一项模拟
  $session[simulations][]=array($requête[marié],$requête[enfants],$requête[salaire],$requête[impots]);
  $requête[simulations]=$session[simulations];

   // 以 XML 格式显示模拟结果
  header("Content-type: text/xml");
  include "$simulationsImpots";

   // 结束
  terminerSession($session);

我们之前已经介绍了需要提供的两种响应类型 XML 以及应随附的样式表。应用程序 impots_simulations.php 的代码如下:

<?php
     // 生成税款应用程序模拟页面的代码 XML

   // 若干常量
  $xslSimulations="simulations.xsl";

   // XML 的页眉
  echo "<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>\n";
  echo "<?xml-stylesheet type=\"text/xsl\" href=\"$xslSimulations\" ?>\n";    
  // 模拟
  echo "<simulations>\n";
  for ($i=0;$i<count($requête[simulations]);$i++){
      // 模拟$i
    echo "<simulation marie=\"".$requête[simulations][$i][0]."\" ".
                "enfants=\"".$requête[simulations][$i][1]."\" ".
          "salaire=\"".$requête[simulations][$i][2]."\" ".
          "impot=\"".$requête[simulations][$i][3]."\" />\n";
  }//for
  echo "</simulations>\n";
?>

该代码可基于字典 $requête 生成类似于以下格式的 XML 代码:


<?xml version="1.0" encoding="ISO-8859-1" ?>
<?xml-stylesheet type="text/xsl" href="simulations.xsl" ?>
<simulations>
<simulation marie="non" enfants="3" salaire="200000" impot="22504" />
<simulation marie="oui" enfants="3" salaire="200000" impot="16400" />
<simulation marie="oui" enfants="2" salaire="200000" impot="22504" />
</simulations>

样式表 simulations.xsl 将把代码 XML 转换为代码 HTML。

应用程序代码 impots_erreurs.php 如下:

<?php
     // 生成应用程序 impots 错误页面的代码 XML

   // 一些常量
  $xslErreurs="erreurs.xsl";

   // XML 的头部
  echo "<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>\n";
  echo "<?xml-stylesheet type=\"text/xsl\" href=\"$xslErreurs\" ?>\n";
   // 错误
  echo "<erreurs>\n";
  for ($i=0;$i<count($requête[erreurs]);$i++){
      // 错误$i
    echo "<erreur>".$requête[erreurs][$i]."</erreur>";
  }//for
  echo "</erreurs>\n";
?>

该代码可基于字典 $requête 生成类似于以下格式的 XML 代码:

<?xml version="1.0" encoding="ISO-8859-1" ?>
<?xml-stylesheet type="text/xsl" href="erreurs.xsl" ?>
<erreurs>
<erreur>Impossible d'ouvrir la base DSN [mysql-dbimpots] (S1000)</erreur>
</erreurs>

样式表 erreurs.xsl 将把代码 XML 转换为代码 HTML。

让我们来看一个简单的示例:

Image

SGBD MySQL 未被调用。因此,我们收到以下响应:

Image

如果查看浏览器接收到的源代码,会看到如下内容:

<?xml version="1.0" encoding="ISO-8859-1" ?>
<?xml-stylesheet type="text/xsl" href="erreurs.xsl" ?>
<erreurs>
<erreur>Impossible d'ouvrir la base DSN [mysql-dbimpots] (S1000)</erreur>
</erreurs>

现在,我们运行 SGBD MySQL 并进行一系列不同的模拟。我们得到以下结果:

Image

浏览器接收到的代码如下:

<?xml version="1.0" encoding="ISO-8859-1" ?>
<?xml-stylesheet type="text/xsl" href="simulations.xsl" ?>
<simulations>
<simulation marie="oui" enfants="2" salaire="200000" impot="22504" />
<simulation marie="non" enfants="2" salaire="200000" impot="33388" />
<simulation marie="non" enfants="3" salaire="200000" impot="22504" />
<simulation marie="oui" enfants="3" salaire="200000" impot="16400" />
</simulations>

值得注意的是,我们的新应用程序比之前的版本更易于维护。部分工作已转移到样式表 XSL 上。 这种新的任务分配方式的优势在于:一旦确定了响应的 XML 格式,样式表的开发就与应用程序的开发相互独立。

5.3. 将 XML 文档解析为 PHP

我们税务应用程序的下一个版本将作为前一版本 xmlsimulations 的客户端程序。因此,我们的客户端将接收 XML 格式的数据,并需对其进行解析以提取所需信息。 在此,我们将暂停对各版本的介绍,学习如何将 XML 文档解析为 PHP。我们将通过以下示例进行演示:


dos>e:\php43\php.exe xmlParser.php
syntaxe : xmlParser.php fichierXML

xmlParser.php应用程序支持一个参数:待分析文档XML的URI(统一资源标识符)。 在本例中,该 URI 仅为位于应用程序目录中的 XML 文件的名称。下面看两个运行示例。 在第一个示例中,被分析的 XML 文件是以下 erreurs.xml 文件:

<?xml version="1.0" encoding="ISO-8859-1"?>
<?xml-stylesheet type="text/xsl" href="erreurs.xsl"?>
<erreurs>
    <erreur>erreur 1</erreur>
  <erreur>erreur 2</erreur>
</erreurs>

分析结果如下:


dos>e:\php43\php.exe xmlParser.php erreurs.xml
ERREURS
 ERREUR
  [erreur 1]
 /ERREUR
 ERREUR
  [erreur 2]
 /ERREUR
/ERREURS

我们尚未说明xmlParser.php应用程序的功能,但从这里可以看出,它显示了已分析文档XML的结构。 第二个示例分析了以下文件:XML simulations.xml

<?xml version="1.0" encoding="ISO-8859-1"?>
<?xml-stylesheet type="text/xsl" href="simulations.xsl"?>
<simulations>
  <simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>
  <simulation marie="non" enfants="2" salaire="200000" impot="33388"/>
</simulations>

分析结果如下:


dos>e:\php43\php.exe xmlParser.php simulations.xml
SIMULATIONS
 SIMULATION,(MARIE,oui) (ENFANTS,2) (SALAIRE,200000) (IMPOT,22504)
 /SIMULATION
 SIMULATION,(MARIE,non) (ENFANTS,2) (SALAIRE,200000) (IMPOT,33388)
 /SIMULATION
/SIMULATIONS

xmlParser.php 应用程序包含了我们在税务应用程序中所需的一切,因为它能够识别 Web 服务器可能发送的错误或模拟数据。让我们来看看它的代码:

<?php
   // 语法 $0 fichierXML
   // 显示文件的结构和内容 fichierXML

   // 调用验证
  if(count($argv)!=2){
       // 错误消息
    fwrite(STDERR,"syntaxe : $argv[0] fichierXML\n");
     // 结束
    exit(1);
  }//if

   // 初始化
  $file=$argv[1];       // XML 文件
  $depth=0;               // 缩进级别=树结构中的深度

   // 程序

   // 创建一个 XML 文本解析对象
  $xml_parser=xml_parser_create();

   // 指定在标签开始和结束时执行哪些函数
  xml_set_element_handler($xml_parser,"startElement","endElement");

   // 指定在遇到数据时执行的函数
  xml_set_character_data_handler($xml_parser,"afficheData");

  // 以读取模式打开 XML 文件
  if (! ($fp=@fopen($file,"r"))){
    fwrite(STDERR,"impossible d'ouvrir le fichier xml $file\n");
    exit(2);
  }//if
   // 以4096字节为单位处理XML文件
  while($data=fread($fp,4096)){
    // 分析读取的数据
    if (! xml_parse($xml_parser,$data,feof($fp))){
      // 发生错误
      fprintf(STDERR,"erreur XML : %s à la ligne %d\n",
        xml_error_string(xml_get_error_code($xml_parser)),
        xml_get_current_line_number($xml_parser));
       // 结束
      exit(3);
    }//if
  }//while
  // 文件已遍历完毕
   // 释放 XML 解析器占用的资源
  xml_parser_free($xml_parser);
   // 结束
  exit(0);

    // -----------------------------------------------------------  
   // 遇到开始标签时调用的函数
  function startElement($parser,$name,$attributs){
    global $depth;
     // 一连串空格(缩进)
    for($i=0;$i<$depth;$i++){
      print " ";
    }//for
     // 属性
    $précisions="";
    while(list($attrib,$valeur)=each($attributs)){
      $précisions.="($attrib,$valeur) ";
    }
     // 显示标签名称及可能的属性
    if($précisions)
      print "$name,$précisions\n";
    else print "$name\n";
     // 增加一层树结构
    $depth++;
  }//startElement

    // -----------------------------------------------------------  
   // 遇到结束标签时调用的函数
  function endElement($parser,$name){
     // 标签结束
     // 缩进级别
    global $depth;
    $depth--;
    // 一串空格(缩进)
    for($i=0;$i<$depth;$i++){
      echo " ";
    }//for
     // 标签名称
    echo "/$name\n";
  }//endElement

    // -----------------------------------------------------------  
   // 数据显示函数
  function afficheData($parser,$data){
     // 缩进级别
    global $depth;
     // 显示数据
    $data=trim($data);
    if($data!=""){
       // 一串空格(缩进)
      for($i=0;$i<$depth;$i++){
        echo " ";
      }//for
      echo "[$data]\n";
    }//if
  }//afficheData
?>

让我们研究一下与 XML 相关的代码。为了分析 XML 文档,我们的应用程序需要一个名为 XML 的代码解析器,通常称为“解析器”。

<?php
...
   // 创建一个 XML 文本分析对象
  $xml_parser=xml_parser_create();

当解析器解析文档 XML 时,它会触发诸如:遇到文档开头、标签开头、标签属性、标签内容、标签结尾、文档结尾等事件。它将这些事件传递给我们指定的方法:

<?php
...
   // 指定在标签开始和结束时执行哪些函数
  xml_set_element_handler($xml_parser,"startElement","endElement");
   // 指定在遇到数据时执行的函数
  xml_set_character_data_handler($xml_parser,"afficheData");
解析器
处理方法
début d'un élément :<balise>
function startElement($parser,$name,$attributes)
$parser:文档的解析器
$name:已解析元素的名称。如果遇到的是 <simulations> 元素,则返回 name="simulations"。
$attributs:标签属性的列表,格式为 (ATTRIBUT,值),其中 ATTRIBUT 是标签的大写名称。
valeur d'un élément :
<balise>valeur</balise>
function afficheData($parser,$data)
$parser:文档解析器
$data:标签数据
fin d'un élément : </balise> ou <balise .../>
function endElement($parser,$name)
参数与方法 startElement 相同。

函数 startElement 通过参数 $attributs 获取元素的属性。该参数是标签属性的字典。因此,如果我们有以下标签:

  <simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>

则字典 $attributs 将如下所示:array(marie=>是,children=>2,salary=>200000,tax=>22504)

在定义好解析器和上述方法后,文档的解析由函数 xml_parse 完成:

<?php
...
   // 以 4096 字节为单位处理 XML 文件
  while($data=fread($fp,4096)){
    // 对读取的数据进行分析
    if (! xml_parse($xml_parser,$data,feof($fp))){
...
    }//if
  }//while
analyse d'un
document $doc
function xml_parse($parser, $doc, $fin)
解析器 $parser 正在解析文档 $doc$doc 可能是某个更大文档的一部分。 参数 $fin 用于指示当前片段是否为最后一个(true)或不是(false)。在解析文档 $doc 时,每当遇到标签的开始和结束,都会调用由 xml_set_element_handler 定义的函数。 而由 xml_set_character_data_handler 定义的函数则在每次获取到标签内容时被调用。

在解析文档 XML 时,可能会发生错误,特别是如果文档 XML “格式不正确”,例如遗漏了结束标签。 在这种情况下,函数 xml_parse 返回的值为“false”:

<?php
...
    if (! xml_parse($xml_parser,$data,feof($fp))){
      // 发生错误
      fprintf(STDERR,"erreur XML : %s à la ligne %d\n",
        xml_error_string(xml_get_error_code($xml_parser)),
        xml_get_current_line_number($xml_parser));
      // 结束
      exit(3);
    }//if
gestion d'erreurs
function xml_get_error_code($parser)
返回最近发生的错误代码
--
function xml_error_string($code)
返回与传入参数中的代码相关的错误信息
--
function xml_get_current_line($parser)
返回解析器当前正在解析的文档 XML 的行号

当文档解析完成后,释放分配给解析器的资源:

<?php
...
   // 释放 XML 解析器占用的资源
  xml_parser_free($xml_parser);
libération des
ressources attribuées
au parseur $parser
function xml_free($parser)

解释完这一点后,前面的程序及其运行示例便不言自明。

5.4. 税务应用程序:第6版

现在我们已具备所有要素,可以编写一个客户端程序来调用我们的税务服务,该服务返回 XML 格式数据。我们将采用应用程序的第 4 版来构建客户端,而服务器端则继续使用第 5 版。在此客户端-服务器应用程序中:

  • 税务计算模拟服务由应用程序 xmlsimulations.php 提供。因此,服务器的响应格式为 XML,正如我们在第 5 版中所见。
  • 客户端不再是浏览器,而是一个独立的PHP客户端。其控制台界面与第4版相同。

建议读者重新阅读 cltImpots.php 应用程序的代码,该程序是第 4 版的客户端。该客户端从服务器接收 $document 文档,而该文档当时是 HTML 文档。 现在则为 XML 文档。HTML 和 $document 文档由以下函数进行解析:

<?php
...
  function getInfos($document){
       // $document : 文档 HTML
     // 查询错误列表
     // 或模拟表

     // 结果准备
    $impots[erreurs]=array();
    $impots[simulations]=array();

........
    return $impots;
    }//getInfos

该函数接收文档 HTML $document,对其进行分析,并返回一个包含两个属性的字典 $impots

  1. 错误:一个错误数组
  2. 模拟:一个模拟数组,每个模拟本身是一个包含四个元素的数组(配偶、子女、工资、税款)。

应用程序 cltImpots.php 现更名为 cltXmlSimulations.php。仅需修改处理服务器返回文档的部分,以适应文档格式已变为 XML 的情况。 因此,函数 getInfos 变为如下形式:

<?php
...
// --------------------------------------------------------------
  function getInfos($document){
       // $document:文档 XML
     // 我们正在寻找错误列表
     // 或模拟表

    global $impots,$balises;

     // 结果准备
    $impots[erreurs]=array();
    $impots[simulations]=array();
     // 正在处理标签
    $balises=array();

     // 创建一个 XML 文本分析对象
    $xml_parser=xml_parser_create();
     // 指定标签开始和结束时要执行的函数
    xml_set_element_handler($xml_parser,"startElement","endElement");
     // 指定在遇到数据时执行的函数
    xml_set_character_data_handler($xml_parser,"getData");
    // 对 $document 进行解析
    xml_parse($xml_parser,$document);
     // 释放 XML 解析器占用的资源
    xml_parser_free($xml_parser);
     // 结束
    return $impots;
  }//getInfos

    // -----------------------------------------------------------  
  // 遇到开始标签时调用的函数
  function startElement($parser,$name,$attributs){
    global $impots,$balise,$balises,$contenu;

     // 记录标签名称及其内容
    $balise=strtolower($name);
    $contenu="";
     // 将其添加到标签堆栈中
    array_push($balises,$balise);    
    // 这是模拟吗?
    if($balise=="simulation"){   
         // 记录模拟的属性
      $impots[simulations][]=array($attributs[MARIE],$attributs[ENFANTS],$attributs[SALAIRE],$attributs[IMPOT]);
    }//if
  }//startElement

    // -----------------------------------------------------------  
   // 遇到结束标记时调用的函数
  function endElement($parser,$name){
       // 获取当前标签
    global $impots,$balises,$contenu;
    $balise=array_pop($balises);
    // 这是错误标签吗?
    if($balise=="erreur"){
         // 又一个错误
      $impots[erreurs][]=trim($contenu);
    }//if
  }//endElement

    // -----------------------------------------------------------  
   // 标签内容的处理函数
  function getData($parser,$data){
      // 全局数据
    global $balise,$contenu;
     // 这是错误标签吗?
    if($balise=="erreur"){
         // 累加到当前标签的内容中
      $contenu.=$data;
    }//if
  }//getData

注释:

  • 函数 getInfos($document) 首先创建一个解析器,然后对其进行配置,最后开始分析文档 $document
<?php
...
     // 创建一个 XML 文本解析对象
    $xml_parser=xml_parser_create();
     // 指定在标签开始和结束时执行哪些函数
    xml_set_element_handler($xml_parser,"startElement","endElement");
     // 指定在遇到数据时执行的函数
    xml_set_character_data_handler($xml_parser,"getData");
    // 对 $document 进行解析
    xml_parse($xml_parser,$document);
  • 分析完成后,释放分配给解析器的资源,并返回字典 $impots
<?php
...
     // 释放 XML 解析器占用的资源
    xml_parser_free($xml_parser);
     // 结束
    return $impots;
  • 函数 startElement($parser,$name,$attributs) 在每个标签开始时被调用。它
    • 在标签数组 $balises 中发现标签 $name。该数组以栈的形式管理:当遇到标签结束符号时,$balises 中栈顶的最后一个标签将被弹出。 当前标签也会记录在 $balise 中。在字典 $attributs 中,可以找到所遇标签的属性,这些属性以大写形式存储。
    • 若为模拟标签,则将属性记录在全局字典 $impots[simulations] 中。
  • 当标签的内容 $data 被检索后,调用函数 getData($parser,$data)。 这里我们采取了一项预防措施。在某些处理 XML 文档的 API 中(特别是 Java),指出该函数可以被反复调用,c.a.d。 这意味着我们未必能一次性获取标签的全部内容。 此处的文档 PHP 并未说明这一限制。为谨慎起见,我们将获取到的值 $data 累加到全局变量 $contenu 中。 只有在遇到标签结束符号时,才认为已获取该标签的全部内容。受此处理影响的唯一标签是 <erreur>
  • 在每个标签结束时都会调用函数 endElement($parser,$name)。 此处调用该函数是为了通过移除标签堆栈中的最后一个标签来更改当前标签的名称,并添加以 <erreur> 结尾的标签内容至 $impots[erreurs] 数组中。

以下是几个执行示例,首先是一个未被调用的 SGBD:

dos>e:\php43\php.exe cltXmlSimulations.php http://localhost/poly/impots/8/xmlsimulations.php 是 2 200000
Jeton de session=[e8c29ea12f79e4771960068d161229fd]
Les erreurs suivantes se sont produites :
Impossible d'ouvrir la base DSN [mysql-dbimpots] (S1000)

随后是已运行的 SGBD:


dos>e:\php43\php.exe cltXmlSimulations.php http://localhost/poly/impots/8/xmlsimulations.php 是 3 200000
Jeton de session=[69a54d79db10b70ed0a2d55d5026ac8b]
Simulations :
[oui,3,200000,16400]

dos >e:\php43\php.exe cltXmlSimulations.php http://localhost/poly/impots/8/xmlsimulations.php 是 2 200000 69a54d79db10b70ed0a2d55d5026ac8b
Jeton de session=[69a54d79db10b70ed0a2d55d5026ac8b]
Simulations :
[oui,3,200000,16400]
[oui,2,200000,22504]

dos >e:\php43\php.exe cltXmlSimulations.php http://localhost/poly/impots/8/xmlsimulations.php 否 2 200000 69a54d79db10b70ed0a2d55d5026ac8b
Jeton de session=[69a54d79db10b70ed0a2d55d5026ac8b]
Simulations :
[oui,3,200000,16400]
[oui,2,200000,22504]
[non,2,200000,33388]

5.5. 结论

得益于其响应机制,税费应用程序对于其设计者以及客户端应用程序的设计者而言,都变得更易于管理。

  • 现在,服务器应用程序的设计可以委托给两类人员:Servlet 开发人员和负责管理浏览器中响应外观的图形设计师。 后者只需了解服务器响应的结构,即可构建相应的样式表。 需要指出的是,这些样式表存储在独立的XSL文件中,与PHP应用程序相互独立。因此,图形设计师可以独立于开发人员开展工作。
  • 客户端应用程序的设计人员也只需了解服务器响应的结构即可。图形设计师对样式表所做的任何修改都不会影响该响应,该响应始终保持不变。这是一个巨大的优势。
  • 开发人员如何在不破坏现有架构的情况下升级其PHP应用程序? 首先,只要响应 XML 保持不变,开发者便可按需组织应用程序。此外,只要保留客户端所需的 <error> <simulation> 元素,开发者也可对响应 XML 进行升级。这样,开发者便可在该响应中添加新标签。 前端设计师会在样式表中考虑这些新标签,浏览器也能获取响应的新版本。而已编程的客户端将继续使用旧模板,新标签会被直接忽略。要实现这一点,必须在对服务器响应的XML解析中,准确识别出所查找的标签。 这正是我们在税务应用程序的XML客户端中所做的:在相关流程中,我们明确指定处理<erreur>和<simulation>标签。因此,其他标签会被忽略。