管理后台

MVC原理

我们先以后台登陆为例说明 osCommerce V3 的管理后台 MVC 工作原理,登陆 URL 为: admin/index.php?login

主执行文件 admin/index.php

require('includes/application_top.php'); //加载必须的类与函数库,并进行权限检查 
require('includes/classes/template.php'); // 加载模板类

$_SESSION['module'] = 'index'; // 默认模块为 index
if ( !empty($_GET) ) { // 如果设置了模块名称,则使用相应的模块
  $first_array = array_slice($_GET, 0, 1); // 模块名为$_GET 参数的第一个参数,且只有 KEY,没有 VALUE
  $_module = osc_sanitize_string(basename(key($first_array))); //以登陆 URL “admin/index.php?login”为例,此处的模块名应为“login”

  if ( file_exists('includes/applications/' . $_module . '/' . $_module . '.php') ) { // 进一步确认模块文件是否有效
    $_SESSION['module'] = $_module; // 设置“login”为当前的模块名 }
  }
}

if ( !osC_Access::hasAccess($_SESSION['module']) ) { // 模测对当前模块是否有操作权限 
  $osC_MessageStack->add('header', 'No access.', 'error');
  osc_redirect_admin(osc_href_link_admin(FILENAME_DEFAULT));
}

$osC_Language->loadIniFile($_SESSION['module'] . '.php'); // 加载语言文件

require('includes/applications/' . $_SESSION['module'] . '/' . $_SESSION['module'] . '.php');

// 加载模块类,管理后台的所有模块类都继承自 osC_Template_Admin 后台模块类,而 osC_Template_Admin 类又是 osC_Template 类的子类,所以后台模板系统采用了前台模板的框架,加上后 台的标准化,也就使得后台 MVC 结构可以更灵活,实现的代码反而更少。

$osC_Template = osC_Template_Admin::setup($_SESSION['module']); // 配置模板 
$osC_Template->set('default'); // 设置当前模板

require('templates/default.php'); // 加载模板
require('includes/application_bottom.php');

osC_Template_Admin 类 admin/includes/classes/template.php

require('../includes/classes/template.php');

class osC_Template_Admin extends osC_Template {
  function &setup($module) { // 配置模板方法
    $class = 'osC_Application_' . ucfirst($module);
    if ( isset($_GET['action']) && !empty($_GET['action']) ) { // 由$_GET 参数“action ”指定动作
      $_action = osc_sanitize_string(basename($_GET['action']));

      if ( file_exists('includes/applications/' . $module . '/actions/' . $_action . '.php') ) {
        include('includes/applications/' . $module . '/actions/' . $_action . '.php'); // 动作保存的目录是“includes/applications/MODULE/actions/ACTION.php”,其中 MODULE 为模块名,ACTION 为动作名
        $class = 'osC_Application_' . ucfirst($module) . '_Actions_' . $_action; // 动作类的名称规则是“osC_Application_MODULE_Actions_ACTION” }
      }

      $object = new $class();

      // 动作的代码就放置在其初始化方法里,一般在执行完成后会跳转到新 的页面,从而下面的代码将不会被执行,只有动作执行出错时或者该动作只是为了预处理相关信息,才会继续下面的步骤
      // 当没有设置$_GET[‘action’]时,则不会执行动作,而是会转向到显示模块内容,当我们需要登陆时,此 时的$class 等于“osC_Application_Login”
      return $object;
    }
  }

osC_Application_Login 登陆类 admin/includes/applications/login/login.php

class osC_Application_Login extends osC_Template_Admin {
  /* Protected variables */
  // 每个后台模块类大体都较简单,因为它们继承自 osC_Template_Admin 类,从而间接地继承于 osC_Template 类,所以代码都比较精简。模块类最重要的参数便是以下三个:

  protected $_module = 'login', // $_module指定当前的模块名
  $_page_title, // $_page_title指定当前的标题
  $_page_contents = 'main.php'; // $_page_contents指定内容模块的文件名,内容模块位于“includes/applications/MODULE/pages/”

  /* Class constructor */
  public function __construct() {
    global $osC_Language;

    $this->_page_title = $osC_Language->get('heading_title'); // 从语言文件中读取标 题
  }
}

显示模板 admin/templates/default.php [79]

<div class="pageContents">
<?php require('includes/applications/' . $osC_Template->getModule() . '/pages/' . $osC_Template->getPageContentsFilename()); ?>
</div>

除去加载 header 和 footer 的部分,最主要的代码便是上面一段,这段代码决定了版面中将要 以什么内容为主角。

如下面的代码所示, osC_Template 类的 getModule 方法会返回后台模块类的$_module 变量, osC_Template 类的 getPageContentsFilename 方法会返回后台模块类的$_page_contents 变量的值。

因此当我们的 URL 为“ admin/index.php?login ”时,其显示的中心内容将是 “includes/applications/login/pages/main.php”文件的内容,而该文件的内容正是要显示一个登陆表单

osC_Template 类的 getModule 方法 includes/classes/template.php[253]

function getModule() {
  return $this->_module;
}

osC_Template 类的 getPageContentsFilename 方法 includes/classes/template.php [344]

function getPageContentsFilename() {
  return $this->_page_contents;
}

处理登陆

当我们输入了用户名和密码,进行登陆时,我们的 URL 是 “admin/index.php?login&action=process”,同时通过$_POST 将用户名和密码也传输过来。

与显示登陆界面的不同之处在于:当执行到 osC_Template_Admin 类的 setup 方法时,上面提 到会检测 $_GET[‘action’] 值,以此来判断是否需要执行动作,在我们的 URL “admin/index.php?login&action=process”里,“process”会被作为动作名,从而会加载动作类 osC_Application_Login_Action_process (位于“includes/applications/login/actions/process.php”) , 同时对其进行实例化。

osC_Application_Login_Actions_process 类 admin/includes/applications/login/actions/process.php

class osC_Application_Login_Actions_process extends osC_Application_Login {
  public function __construct() {
    global $osC_Database, $osC_Language, $osC_MessageStack;

    parent::__construct();
    if ( !empty($_POST['user_name']) && !empty($_POST['user_password']) ) {
      $Qadmin = $osC_Database->query('select id, user_name, user_password from :table_administrators where user_name = :user_name');
      $Qadmin->bindTable(':table_administrators', TABLE_ADMINISTRATORS);
      $Qadmin->bindValue(':user_name', $_POST['user_name']);
      $Qadmin->execute();
      if ( $Qadmin->numberOfRows() ) { // 查找是否存在指定的用户名 
        if ( osc_validate_password($_POST['user_password'], $Qadmin->value('user_password')) ) { // 比对密码
          $_SESSION['admin'] = array(
            'id' => $Qadmin->valueInt('id'),
            'username' => $Qadmin->value('user_name'),
            'access' => osC_Access::getUserLevels($Qadmin->valueInt('id'))
          ); // 加载用户的权限

          $get_string = null;
          if ( isset($_SESSION['redirect_origin']) ) {
            $get_string = http_build_query($_SESSION['redirect_origin']['get']);
            unset($_SESSION['redirect_origin']);
          }

          osc_redirect_admin(osc_href_link_admin(FILENAME_DEFAULT, $get_string)); // 如果登陆成功,则会自动跳转到登陆前的页面
        }
      }
    }

    $osC_MessageStack->add('header', $osC_Language->get('ms_error_login_invalid'), 'error'); // 如果登陆失败,则会输出错误信 息
  }
}

总结以上的介绍,可以得出下面的类关系图:

页面的切换

页面的切换需要先由动作触发,我们以编辑币种为例。

osC_Application_Currencies_Actions_save 类 admin/includes/application/currencies/actions/save.php

class osC_Application_Currencies_Actions_save extends osC_Application_Currencies
{
  public function __construct() {
    global $osC_Language, $osC_MessageStack;

    parent::__construct();

    if ( isset($_GET['cID']) && is_numeric($_GET['cID']) ) { // 当触发 osC_Application_Currencies 类的动作 save 时,有两种可能:一是编辑现有币种,第二是新建币种
      $this->_page_contents = 'edit.php'; // 通过设置内部变量$_page_contents 的值,即 可以更改主内容
    } else {
      $this->_page_contents = 'new.php'; // 显示新建币种的页面
    }

    if ( isset($_POST['subaction']) && ($_POST['subaction'] == 'confirm') ) { // 当 提交了特定的参数,表示编辑完成,需要提交输入的内容,此处便进入了动作的调用
       $data = array(
        'title' => $_POST['title'],
        'code' => $_POST['code'],
        'symbol_left' => $_POST['symbol_left'],
        'symbol_right' => $_POST['symbol_right'],
        'decimal_places' => $_POST['decimal_places'],
        'value' => $_POST['value']
      );

      if ( osC_Currencies_Admin::save((isset($_GET['cID']) && is_numeric($_GET['cID']) ? $_GET['cID'] : null), $data, ((isset($_POST['default']) && ($_POST['default'] == 'on')) || (isset($_POST['is_default']) && ($_POST['is_default'] == 'true') && ($_POST['code'] != DEFAULT_CURRENCY)))) ) {
        $osC_MessageStack->add($this->_module,
$osC_Language->get('ms_success_action_performed'), 'success');
      } else {
        $osC_MessageStack->add($this->_module,

        $osC_Language->get('ms_error_action_not_performed'), 'error');
      }

      osc_redirect_admin(osc_href_link_admin(FILENAME_DEFAULT, $this->_module));
    }
  }
}

下面的流程图,显示了动作类区分显示内容与执行动作的过程。

子模块的调用

单独的模块在 osCommerce V3 里是怎么调用的呢?其实是通过传递$_GET[‘module’]参数来 指定的,当然在代码方面还必须给予相关的支持。

因为在 osCommerce V3 里统计分为“低库存”、“销售统计”、“畅销产品统计”和“产品浏览统计”,所以使用了模块来进行管理。下面便以“低库存统计”为例说明模块的调用过程

当显示统计页面时,首先调用的是统计模板类:

osC_Application_Statistics 类 admin/includes/applications/statistics/statistics.php

class osC_Application_Statistics extends osC_Template_Admin {
  /* Protected variables */
  protected $_module = 'statistics',
    $_page_title,
    $_page_contents = 'main.php'; // 文件 main.php 将负责显示子模块的内容

  /* Class constructor */
  function __construct() {
    global $osC_Language;

    $this->_page_title = $osC_Language->get('heading_title'); // 从语言文件里读取标 题
    if ( !isset($_GET['module']) ) { // 如果设置了$_GET[‘module’],则表示点击了详细的统 计项,需要进一步显示详细的内容,如“低库存统计”传递的$_GET[‘module’]值等于“low_stock”
      $_GET['module'] = '';
    }

    if ( !isset($_GET['page']) || ( isset($_GET['page']) && !is_numeric($_GET['page']) ) ) { // 跟踪页码
      $_GET['page'] = 1;
    }

    if ( !empty($_GET['module']) && !file_exists('includes/modules/statistics/' . $_GET['module'] . '.php') ) {
      $_GET['module'] = '';
    }

    if ( empty($_GET['module']) ) { // 除非设置了子模块,否则将显示出所有可用子模块,供用 户选择
      $this->_page_contents = 'listing.php'; // 文件“listing.php”列示出所有可用的子模块
    }
  }
}

初始化子模块 admin/includes/applications/statistics/pages/main.php[15]

include('includes/modules/statistics/' . $_GET['module'] . '.php'); //加载子模块, 子模块的保存位置是“admin/includes/modules/MODULE/”,其中 MODULE 是当前模板类名称,如“ statistics”
$class = 'osC_Statistics_' . str_replace(' ', '_', ucwords(str_replace('_', ' ', $_GET['module']))); // 子模块类名规则:osC_Statistics_ + 当前模板类名 + 子模块名
$osC_Statistics = new $class(); // 实例化子模块类 $osC_Statistics->activate(); // 激活子模块,执行必要的数据加载工作

osC_Statistics 类 admin/includes/classes/statistics.php[39]

function activate() {
  $this->_setHeader();
  $this->_setData();
}

通过上面 osC_Statistics 类的 activate 方法,可以了解到它执行了_setData 方法,而负责“低库存 统计”的 osC_Statistics_Low_Stock 类在统计低库存数据时正是通过_setData 方法实现的

osC_Statistics_Low_Stock 类 admin/includes/modules/statistics/low_stock.php[51]

function _setData() {
  global $osC_Database, $osC_Language;

  $this->_data = array();
  $this->_resultset = $osC_Database->query('select p.products_id, pd.products_name, products_quantity from :table_products p, :table_products_description pd where p.products_id = pd.products_id and pd.language_id = :language_id and p.products_quantity <= :stock_reorder_level order by p.products_quantity desc');
  $this->_resultset->bindTable(':table_products', TABLE_PRODUCTS);
  $this->_resultset->bindTable(':table_products_description', TABLE_PRODUCTS_DESCRIPTION);
  $this->_resultset->bindInt(':language_id', $osC_Language->getID());
  $this->_resultset->bindInt(':stock_reorder_level', STOCK_REORDER_LEVEL);
  $this->_resultset->setBatchLimit($_GET['page'], MAX_DISPLAY_SEARCH_RESULTS);

  $this->_resultset->execute();
  while ( $this->_resultset->next() ) {
    $this->_data[] = array(osc_link_object(osc_href_link_admin(FILENAME_DEFAULT, 'products&pID=' . $this->_resultset->valueInt('products_id') . '&action=preview'), $this->_icon . '&nbsp;' . $this->_resultset->value('products_name')), $this->_resultset->valueInt('products_quantity')); // 将数据保存至内部变量$_data
  }
}

显示子模块内容 admin/includes/applications/statistics/pages/main.php [67]

<?php
foreach ( $osC_Statistics->getData() as $data ) { // osC_Statistics 类的 getData 方法会得到其内部变量$_data 的值
if ( !isset($columns) ) {
  $columns = sizeof($data);
} ?>
<tr onmouseover="rowOverEffect(this);" onmouseout="rowOutEffect(this);">
<?php
for ( $i = 0; $i < $columns; $i++ ) {
  echo '<td>' . $data[$i] . '</td>' . "\n";
}
?>
</tr>
<?php } ?>

### RPC原理

RPC(全称:Remote Procedure Call)远程过程调用,也是osCommerce V3里新增的功能。 RPC 可以允许其它程序控制 osCommerce 系统的操作,例如获取产品分类、删除指定产品资料, 或者备份数据库等,可以说凡是可以通过后台操作的功能,只要实现了 RPC,便可以通过远程进行操作。

osCommerce V3 RPC 的调用是由后台的“rpc.php”文件开始的:

远程过程调用 admin/rpc.php

```php
header('Cache-Control: no-cache, must-revalidate'); 
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); // 设置为不进行缓存
require('includes/application_top.php'); // RPC调用结果标志符
define('RPC_STATUS_SUCCESS', 1); // 执行成功 
define('RPC_STATUS_NO_SESSION', -10); // 未登陆/登陆已过期 define('RPC_STATUS_NO_MODULE', -20); // 未设置模块/模块不正确 define('RPC_STATUS_NO_ACCESS', -50); // 权限受限 define('RPC_STATUS_CLASS_NONEXISTENT', -60); //不存在指定的类 define('RPC_STATUS_NO_ACTION', -70); // 未设置动作 define('RPC_STATUS_ACTION_NONEXISTENT', -71); // 动作不存在

if ( !isset($_SESSION['admin']) ) { // 判断登陆
  echo json_encode(array('rpcStatus' => RPC_STATUS_NO_SESSION)); exit;
}

$module = null;
$class = null;
if ( empty($_GET) ) { // 判断模块
  echo json_encode(array('rpcStatus' => RPC_STATUS_NO_MODULE));
  exit;
} else {
  $first_array = array_slice($_GET, 0, 1);
  $_module = osc_sanitize_string(basename(key($first_array))); // 模块为$_GET[0]
  if ( !osC_Access::hasAccess($_module) ) { // 判断操作权限
    echo json_encode(array('rpcStatus' => RPC_STATUS_NO_ACCESS)); exit;
  }

  $class = (isset($_GET['class']) && !empty($_GET['class'])) ? osc_sanitize_string(basename($_GET['class'])) : 'rpc'; // 默认调用类“rpc”,否则使用 $_GET[‘class’]指定类名
  $action = (isset($_GET['action']) && !empty($_GET['action'])) ? osc_sanitize_string(basename($_GET['action'])) : ''; // 使用$_GET[‘action’]指定动作 (即类的方法)

  if ( empty($action) ) {
    echo json_encode(array('rpcStatus' => RPC_STATUS_NO_ACTION));
    exit;
  }

  if ( file_exists('includes/applications/' . $_module . '/classes/' . $class . '.php')) {
    include('includes/applications/' . $_module . '/classes/' . $class . '.php'); // 加载类
    if ( method_exists('osC_' . ucfirst($_module) . '_Admin_' . $class, $action) ) {
      call_user_func(array('osC_' . ucfirst($_module) . '_Admin_' . $class,
$action)); // 调用类的方法,结果一般为 JSON 代码 
      exit;
    } else {
      echo json_encode(array('rpcStatus' => RPC_STATUS_ACTION_NONEXISTENT));
      exit;
    }
  } else {
    echo json_encode(array('rpcStatus' => RPC_STATUS_CLASS_NONEXISTENT));
    exit;
  }
}

实例:

osC_Countries_Admin_rpc 国家 RPC 类 admin/includes/applications/countries/classes/rpc.php

class osC_Countries_Admin_rpc {
  public static function getAll() { // 方法 getAll 获取所有国家列表或者搜索指定国家
    if ( !isset($_GET['search']) ) { // $_GET[‘search’]指定需要搜索的国家 $_GET
      ['search'] = '';
    }

    if ( !isset($_GET['page']) || !is_numeric($_GET['page']) ) { // 指定页码 
      $_GET['page'] = 1;
    }

    if ( !empty($_GET['search']) ) {
      $result = osC_Countries_Admin::find($_GET['search'], $_GET['page']); // 查找指定国家
    } else {
      $result = osC_Countries_Admin::getAll($_GET['page']); // 获取指定页码的所有国家
    }

    $result['rpcStatus'] = RPC_STATUS_SUCCESS; // 结果会是一个关联数组,结构为 array(‘entries‘=>包含所有数据的数组,’total’=>结果数,’ rpcStatus’=>1)
    echo json_encode($result); // 将结果进行 JSON 编码
  }

  public static function getAllZones() { // getAllZones 方法获取指定国家的所有省份/区 信息,或者搜索指定国家内的省份/区信息
    global $_module;

    if ( !isset($_GET['search']) ) {
      $_GET['search'] = '';
    }

    if ( !empty($_GET['search']) ) {
      $result = osC_Countries_Admin::findZones($_GET['search'], $_GET[$_module]);
    } else {
      $result = osC_Countries_Admin::getAllZones($_GET[$_module]);
    }

    $result['rpcStatus'] = RPC_STATUS_SUCCESS;
    echo json_encode($result);
  }
}

osC_Countries_Admin 类 admin/includes/applications/countries/classes/coutries.php[37]

public static function getAll($pageset = 1) {// 获取某一页的所有国家
  global $osC_Database;
  if ( !is_numeric($pageset) || (floor($pageset) != $pageset) ) {
    $pageset = 1;
  }

  $result = array('entries' => array());
  $Qcountries = $osC_Database->query('select SQL_CALC_FOUND_ROWS * from  :table_countries order by countries_name');
  $Qcountries->bindTable(':table_countries', TABLE_COUNTRIES);
  if ( $pageset !== -1 ) {
    $Qcountries->setBatchLimit($pageset, MAX_DISPLAY_SEARCH_RESULTS);
  }

  $Qcountries->execute();
  while ( $Qcountries->next() ) {
    $Qzones = $osC_Database->query('select count(*) as total_zones from :table_zones where zone_country_id = :zone_country_id');
    $Qzones->bindTable(':table_zones', TABLE_ZONES);
    $Qzones->bindInt(':zone_country_id',
    $Qcountries->valueInt('countries_id'));
    $Qzones->execute();

    $result['entries'][] = array_merge($Qcountries->toArray(), $Qzones->toArray());// 将国家及该国所有省份/区信息存放于$result[‘entries’]
  }

  $result['total'] = $Qcountries->getBatchSize(); // $result['total']保存结果数
  if ( $Qcountries->numberOfRows() > 0 ) {
    $Qzones->freeResult();
  }

  $Qcountries->freeResult();
  return $result;
}

public static function find($search, $pageset = 1) { // 查找指定页码里的国家
  global $osC_Database;

  if ( !is_numeric($pageset) || (floor($pageset) != $pageset) ) {
    $pageset = 1;
  }

  $result = array('entries' => array());
  $Qcountries = $osC_Database->query('select SQL_CALC_FOUND_ROWS c.* from :table_countries c left join :table_zones z on (z.zone_country_id = c.countries_id) where (c.countries_name like :countries_name or c.countries_iso_code_2 like :countries_iso_code_2 or c.countries_iso_code_3 like :countries_iso_code_3 or z.zone_name like :zone_name or z.zone_code like :zone_code) group by c.countries_id order by c.countries_name');
  $Qcountries->bindTable(':table_countries', TABLE_COUNTRIES); 
  $Qcountries->bindTable(':table_zones', TABLE_ZONES); 
  $Qcountries->bindValue(':countries_name', '%' . $search . '%'); // 在国家名称里查找
  $Qcountries->bindValue(':countries_iso_code_2', '%' . $search . '%'); // 或者在ISO CODE2里查找
  $Qcountries->bindValue(':countries_iso_code_3', '%' . $search . '%'); // 又或者在ISO CODE3里查找
  $Qcountries->bindValue(':zone_name', '%' . $search . '%'); // 或者使用省份名查找 
  $Qcountries->bindValue(':zone_code', '%' . $search . '%'); // 或者使用省份编码查找

  if ( $pageset !== -1 ) {
    $Qcountries->setBatchLimit($pageset, MAX_DISPLAY_SEARCH_RESULTS);
  }

  $Qcountries->execute();
  while ( $Qcountries->next() ) {
    $Qzones = $osC_Database->query('select count(*) as total_zones from :table_zones where zone_country_id = :zone_country_id');
    $Qzones->bindTable(':table_zones', TABLE_ZONES);
    $Qzones->bindInt(':zone_country_id',
    $Qcountries->valueInt('countries_id'));
    $Qzones->execute();
    $result['entries'][] = array_merge($Qcountries->toArray(),
    $Qzones->toArray());
  }

  $result['total'] = $Qcountries->getBatchSize();
  if ( $Qcountries->numberOfRows() > 0 ) {
    $Qzones->freeResult();
  }

  $Qcountries->freeResult();
  return $result;
}

public static function findZones($search, $country_id) { // 查找指定的国家里的省份 /区信息
  global $osC_Database;

  $result = array('entries' => array());
  $Qzones = $osC_Database->query('select SQL_CALC_FOUND_ROWS * from :table_zones where zone_country_id = :zone_country_id and (zone_name like :zone_name or zone_code like :zone_code) order by zone_name');
  $Qzones->bindTable(':table_zones', TABLE_ZONES);
  $Qzones->bindInt(':zone_country_id', $country_id);

  $Qzones->bindValue(':zone_name', '%' . $search . '%');
  $Qzones->bindValue(':zone_code', '%' . $search . '%');
  $Qzones->execute();
  while ( $Qzones->next() ) {
    $result['entries'][] = $Qzones->toArray();
  }

  $result['total'] = $Qzones->numberOfRows();
  $Qzones->freeResult();
  return $result;
}

public static function getAllZones($country_id) {// 获取指定国家的所有省份/区信息 
  global $osC_Database;

  $result = array('entries' => array());
  $Qzones = $osC_Database->query('select * from :table_zones where zone_country_id = :zone_country_id');
  $Qzones->bindTable(':table_zones', TABLE_ZONES);
  $Qzones->bindInt(':zone_country_id', $country_id);
  $Qzones->execute();
  while ( $Qzones->next() ) {
    $result['entries'][] = $Qzones->toArray();
  }
  $result['total'] = $Qzones->numberOfRows();
  $Qzones->freeResult();

  return $result;
}

提示:

osCommerce V3 默认对 RPC 类只提供了获取参数的功能,并且功能非常有限。如果需要完全通过 RPC 控制后台的操作,则需要自行增加 RPC 方法,来处理数据的响应。

results matching ""

    No results matching ""