Odoo高级服务端开发技巧

这是Odoo系列文章的第九篇,完整目录请见最好用的免费ERP系统Odoo 11开发指南

以下开发均假设读者已完成第八篇的代码,并且所有代码更新后均需自行更新方会在客户端看到变化。如未阅读该篇,请参考代码:Chapter 8

本文主要内容有

  • 修改运行指定动作用户
  • 以变更的上下文调用方法
  • 执行原生 SQL 语句
  • 为用户编写向导
  • 定义 onchange 方法
  • 在服务端调用 onchange 方法
  • 基于 SQL 视图定义模型

修改运行指定动作用户

Read More

Odoo调试与自动化测试

这是Odoo系列文章的第八篇,完整目录请见最好用的免费ERP系统Odoo 11开发指南

以下开发均假设读者已完成第七篇的代码,并且所有代码更新后均需自行更新方会在客户端看到变化。如未阅读该篇,请参考代码:Chapter 7

本文主要内容

Read More

Odoo 11 甘特图(Gantt)替代方案-Web timeline

Odoo 从9开始就取消了甘特图(Gantt)视图,据称是因为所使用的库dhtmlxgantt所采用的开放协议与 Odoo 不兼容(说人话就是要付费),因而在社区版中不再包含。Alan在刚接触 Odoo 时觉得这个功能以及 Workflow 在新版中的去除都非常可惜。

那么作为调包侠的我们是否有替代方案呢?其实 OCA 早就添加了一个免费的 Timeline 视图用于弥补这一损失,下载地址见官方的第三方应用市场:Web timeline

关于应用的下载和安排此处就不再赘述,先上图:

Odoo 11 甘特图(Gantt)替代方案探讨-Timeline

Read More

Odoo模块数据

这是Odoo系列文章的第七篇,完整目录请见最好用的免费ERP系统Odoo 11开发指南

以下开发均假设读者已完成第六篇的代码,并且所有代码更新后均需自行更新方会在客户端看到变化。如未阅读该篇,请参考代码:Chapter 6

Odoo 出版社添加

本文主要内容:

  • 使用外部 ID 和命名空间
  • 使用 XML 文件加载数据
  • 使用 noupdate 和 forcecreate 标记
  • 使用 CSV文件加载数据
  • 使用 YAML 文件加载数据
  • 插件更新和数据迁移

本文学习如何在安装时为插件提供数据,包含添加默认值、添加描述、菜单、动作等元数据,另一个重要的知识点就是添加演示数据,在安装时我们勾选 Load demonstration data 便会自动载入演示数据。

Read More

Magento中如何自定义后台登录页面

很多人在使用Magento开发网站时除了会修改默认的后台登录路径外还会希望对后台登录页面甚至是后台进行修改。Alan使用Inchoo的插件并参照Mastering Magento Theme Design一书拼接了一个简单的代码,实现在后台登录页的自定义修改以及在后台页面中放置自己logo的功能。

这里我使用了Pinterest的logo在进行测试,以下是后台登录页面:

Magento自定义后台登录页面

以下是管理员找回密码页面:

Magento自定义找回密码页面

以下是后台替换了logo后的效果:

Magento后台自定义

显然在设计上还有很大的提升空间,所以这里贡献出源代码,供大家修改使用:

下载地址:http://pan.baidu.com/s/1eQKksca

使用方法:

1.解压后将app和skin目录拷贝到Magento的安装根目录下,然后登录后台。访问System>Configuration>Design,会看到下面多出一个Admin Theme的版块,在后面填写alanhou保存

Magento自定义Admin Theme

2.修改logo文件,我们的logo文件保存在<skin/adminhtml/default/alanhou/images/目录下,后台使用的是Pinterest-logo.png,登录页面使用的是Pinterest-logo-login.png,不想要修改源代码的朋友可以直接进行替换。

此外,也可以修改app/design/adminhtml/default/alanhou/template/下login.phtml和forgotpassword.phtml文件中的如下代码部分修改登录页面logo

$this->getSkinUrl('../../default/alanhou/images/Pinterest-logo-login.png') ?>"

修改app/design/adminhtml/default/alanhou/template/page/header.phtml文件中如下代码部分修改后台页面显示logo

echo $this->getSkinUrl('images/Pinterest-logo.png')

所有自定义css代码请在skin/adminhtml/default/alanhou/custom.css文件中进行修改

WordPress主题开发系列之自建模板

WordPress的默认集成有twentyfifteen之类的模板,位置在wp-content/themes下。如果想要安装或者开发主题,都需要在这个目录下来完成。

本例将在wp-content/themes目录下创建一个名为alanhou的文件夹,自开发的模板要求至少有index.php和style.css这两个文件。通常在style.css最上方会有一大段注释,用于在设置主题名称、版本等相关信息。细心的你一定还会发现在后台查看主题时还会有一张图片,这张图片来自主题文件夹下的screeshot.png, 建议大小为880*660px(或相似比例的图片)。

/*
Theme Name: Alan Hou
Theme URI: http://alanhou.org/
Author: Alan Hou
Author URI: http://alanhou.org/
Description: Alan Hou自开发主题,用于研究Wordpress二次开发相关知识
Version: 1.0.1
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Tags: alan, translation-ready, custom-menu
Text Domain: alanhou
*/

完成上述操作后在后台的效果如下

Wordpress Alan自建主题

首先我们先在index.php中加入代码,调用主题CSS,并输出博客的标题和描述,主要用到了bloginfo(), get_option(), wp_header()和wp_footer()方法示例代码如下:

<!doctype html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/htm; charset=<?php bloginfo('charset'); ?>" />
  <?php if(is_home()): $title = get_bloginfo('name'); else: $title = wp_title('', false); endif; if($paged>0):
      $title .= '-第'.$paged.'页';
    endif;
  ?>
  <title><?php echo $title; ?></title>
  <meta name="description" content="<?php bloginfo('description'); ?>" />
  <link rel="stylesheet" href="<?php bloginfo('stylesheet_url') ?>" type="text/css" ?>
  <?php wp_head(); ?>
</head>
<body>

<div id="header" class="fl">

<div id="header-left" class="fl">

<h1><a href="<?php echo get_option('home'); ?>"><?php bloginfo('name'); ?></a></h1>


<div class="description"><?php bloginfo('description'); ?></div>

    </div>


<div id="header-right" class="fr">
      
    </div>

  </div>


<div id="nav">

<ul><?php wp_nav_menu(); ?></ul>

  </div>


<div id="main" class="fl">
  </div>

  <?php wp_footer(); ?>
</body>

通过简单的css控制后就得到如下这样的头部

自定义主题头部

循环输出文章内容







<div id="posts" class="fl">
  <?php //判断是否有文章存在 if(have_posts()): while(have_posts()): //获取文章信息并存入全局变量$post中 the_post(); ?>






<div class="post-item">






<div class="post-title">
      <?php /*输出文章标题和链接*/ ?>
      <a href="<?php the_permalink?>">






<h2><?php the_title(); ?></h2>






</a>
    </div>












<div class="post-content">
      <?php /*输出文章内容*/ ?>
      <?php the_content(); ?>
    </div>












<div class="post-meta">
      <?php /*输出文章所属分类*/ ?>
      <?php the_category(); ?>
      <?php /*输出文章作者*/ ?>
      <?php the_author(); ?>
      <?php /*输出文章发布时间*/ ?>
      <?php the_time('Y-m-d'); ?>
      <?php /*输出编辑文章链接*/ ?>
      <?php edit_post_link('Edit','|',''); ?>
    </div>






  </div>






  <?php endwhile; else: echo "当前博客没有文章可供显示"; endif; ?>
</div>







侧边栏和翻页

调出翻页功能只需使用posts_nav_link()即可,而侧边栏使用get_sidebar(),如








<div class="nav_link fl">
  <?php posts_nav_link(); ?>
</div>







<?php get_sidebar(); ?>

通常需首先在functions.php中注册小工具(可同时注册多个小工具),这样才会在后台外观菜单下出现小工具从而进行拖放编辑,例如:

//注册一个侧边栏的小工具
 register_sidebar(
  array(
    'name' => '侧边栏',
    'before_widget' => '






<div class="sbox">',
    'after_widget' => '</div>







',
    'before_title' => '






<h2>',
    'after_title' => '</h2>







'
  )

在主题根目录下创建sidebar.php,使用dynamic_sidebar()调出后台小工具中设置的内容(该函数中可传入register_sidebar中的name值)








<div class="fr rightbox">
  <?php dynamic_sidebar(); ?>
</div>







但有时为防止小工具中未加入内容或者只是为了提高一些灵活性,也可以在side.php中自行加入相应模块,只需通过is_dynamic_sidebar()进行判断,然后输出相应模块即可。比如:






<div class="fr rightbox">
	<?php if(is_dynamic_sidebar()): dynamic_sidebar(); else: ?>





<div class="sbox">





<h2>分类</h2>










<ul>
			<?php wp_list_cats(); ?>
		</ul>





	</div>










<div class="sbox">





<h2>页面</h2>










<ul>
			<?php wp_list_pages(); ?>
		</ul>





	</div>





	





<div class="sbox">





<h2>注册登录</h2>










<ul>
			<?php wp_register(); ?>
			<?php wp_loginout(); ?>
		</ul>





	</div>





		
	<?php endif; ?>
</div>





常用的封装文件

如同侧边栏一样,头部和底部通常也是相同的,所以较为推荐的做法是在主题目录下创建header.php和footer.php,然后使用get_header()和get_footer()函数来进行调用。

分类页模板:

category.php

如果想要不同分类调取不同分类的话,可以使用category加中间杠后接需使用该模板的分类Id号或别名(slug)来进行命名,如category-2.php或category-wordpress.php

也可以通过在category.php中通过判断id号来调用不同模板,比如

if ( is_category(array(1,2,3)) ) {
  include(TEMPLATEPATH . '/abc.php');
}else{
  include(TEMPLATEPATH . '/def.php');
}

还有一种类似的方法

$cat_ID = get_query_var('cat');
$cat = get_category($cat_ID);
$slug=$name = $cat->slug;

if ( '' !== locate_template(  array("template-parts/category-$slug.php")  ) ) {
     get_template_part( 'template-parts/category', $slug );
}else{
	get_template_part( 'template-parts/category', 'default');
}

 

文章页模板

single.php

除了前面介绍的方法外还可以通过previous_post_link()和next_post_link()来输出上一篇和下一篇文章标题链接,同是还可以自定义文本,如previous_post_link(‘上一篇:%link’)

文章页面后台编辑页面中可以添加自定义栏目,名称和值将存放在wp_postmeta表中,可通过get_post_meta()来获取,如

get_post_meta($post->ID, ‘test’, true),其中true返回字符串,false返回数组

独立页面模板

独立页面默认调用index.php作为模板,可通过新建page.php来作为独立页面模板。如果要单独为某一独立页面设置模板的话,可以在模板根目录中创建page-id.php,其中的id为该独立页面的id号。同样地,也可以采用页面别名page-slug.php的方式。

评论文件

评论文件为根目录下的comments.php,然后在相应位置中通过comments_template()进行调用,与评论相关的数据表为wp_comments, wp_commentmeta。

可以通过comments_open()方法判断是否开启了评论功能,对于文章页面可以通过讨论栏止开启或关闭评论(如未找到请点击右上角显示选项进行勾选显示),

对于有密码保护的页面可使用post_password_required()函数进行判断

if(!comments_open()):
  echo "评论功能已关闭";
elseif(post_password_required()):
  echo "请输入密码查看评论";
elseif(!have_comments()):
  echo "还没有评论,赶紧来评论吧!";
else:
  wp_list_comments();
endif;

comment_registration通过后台设置>讨论下的用户必须注册并登录才可以发表评论开启


if(get_option('comment_registration') && !is_user_logged_in()):
  echo '你必须<a href="'.wp_login_url().'">登录</a>才能发表评论';
elseif(comments_open()):
  comment_form();
endif;

搜索页面

和其它页面一样,如果没有search.php文件,搜索页将会调用index.php作为模板,所以请创建search.php文件作为搜索页面

404页面

和其它页面一样,如果没有404.php文件,404页面将会调用index.php作为模板,所以请创建404.php文件作为404页面

Magento开发系列之十二 默认系统配置

本节并没有太多新内容,更多的是对前面有关系统后台配置的补充。在我们创建新的系统配置路径时,Magento并没有存储默认值,甚至对于一些系统默认配置也是如此,这点可以通过查看core_config_data表来进行验证。

mysql> select * from core_config_data;
+-----------+---------+---------+----------------------------+--------+
| config_id | scope   | scope_id| path                       | value  |
+-----------+---------+---------+----------------------------+--------+
|         1 | default |        0| general/region/display_all | 1      |
... ...

这张表仅存储在后台或基它程序中明确设置的值,而如果请求一个没有进行这个设置的系统配置值的话,Magento会到全局配置文件中去查看默认值。虽然不要求这么做,但为自添加配置变量设置一个默认值是一个不错的习惯。这样做很简单,也防止在获取到空值时产生一些意想不到的效果。

上面提到默认值存放在全局配置文件中,这可能与很多的想法大相径庭,因为大家可能会认为这默认值会存储在用于配置后台的system.xml文件中。为什么要这么做其实大可不必深究,我们可以理解为配置文件中存储常用的值,而stystem.xml中存储着用于修改这些值的界面的配置。

在模块配置文件config.xml里添加一个<default />代码块(如app/code/local/Packagename/Modulename/etc/config.xml)

<config>
    <!-- ... -->
    <default>
    </default>
    <!-- ... -->
</config>

这就是我们用于存储默认值的最上级节点,接下来将配置路径转化成XML格式的树形节点,比如我们为以下配置路径设置默认值:

design/header/welcome

那么在config.xml的代码就会是这样:

<config>
    <!-- ... -->
    <default>
        <design>

<header>
                <welcome>Default welcome msg!</welcome>
            </header>

        </design>
    </default>
    <!-- ... -->
</config>

应用了这个配置后,请求design/header/welcome时,如果没有设定值,就会返回”Default welcome msg!”。这个例子是基本系统默认的配置,我们业看看design/header/welcome的真实配置(文件地址:app/code/core/Mage/Page/etc/config.xml):

<default>
    <design>
        <head translate="default_description" module="page">
            <default_title>Magento Commerce</default_title>
            <default_description>Default Description</default_description>
            <default_keywords>Magento, Varien, E-commerce</default_keywords>
            <default_robots>*</default_robots>
            <default_media_type>text/html</default_media_type>
            <default_charset>utf-8</default_charset>
        </head>

<header translate="welcome" module="page">
            <logo_src>images/logo.gif</logo_src>
            <logo_alt>Magento Commerce</logo_alt>
            <logo_src_small>images/logo.gif</logo_src_small>
            <welcome>Default welcome msg!</welcome>
        </header>

<footer translate="copyright" module="page">
            <copyright>&amp;copy; 2015 Magento Demo Store. All Rights Reserved.</copyright>
        </footer>

    </design>
    <system>
        <media_storage_configuration>
            <allowed_resources>
                <site_favicons>favicon</site_favicons>
            </allowed_resources>
        </media_storage_configuration>
    </system>
</default>

这是所有design/*的默认配置,和我们例子中不同的时这里有一个translate属性:

<head translate="default_description" module="page">

translate和module属性告诉系统哪些节点需要被translate,以及使用哪个模块的Data Helper来进行这一操作。在上例中,welcome节点将被转化为:

Mage::helper('page')->__(...);

前面我们也提到过,如果调用helper类时URI没有传入第二部分,会默认使用data,也就是说下面两句代码是一样的:

Mage::helper('page')->__(...);
Mage::helper('page/data')->__(...);

如果想要转化多个子节点,可以在名称之间用逗号进行分隔,如:

<example translate="foo,baz,bar" module="page">

写在后面

本系列更新到此结束,一共十二篇,大多数代码并非出自笔者之手,而是来自Magento资深大师Alan Storm,笔者只是用自己的理解丰富了一下内容,以有助于国内Magento学习者更为有效地掌握Magento开发相关知识。本系列的终结只是一个阶段性的符号,Magento作为一套强大的电商系统还有很值得探讨和学习的地方,需要大家共同努力去研究和分享,让我们一起努力吧!

Magento开发系列之十一 数据重载和升级

在Magento经常被鼓吹也常被滥用的功能就是重载core中的系统代码,而另一个开发者经常讨论的话题就是升级以及重载对升级的阻碍作用。本节我们就来一起看看重载给版本切换所带来的不便。

需要强调下我们这里的是修改Maento中core的业务逻辑,对于phtml模板文件的修改是非常普遍的。

不论是在Magento中还是其它系统中对升级最不友好的肯定是直接修改源代码,比如想要修改产品模型时,就直接编辑了如下文件:

app/code/core/Mage/Catalog/Model/Product.php

一旦这么做,就直接修改Magento中的基础代码,因而在进行升级时就需要进行逐个文件的合并,这通常都会出问题。此外这种修改还可能返回系统无法识别的数据或者是得到计划外的数据。虽然我们不建议去修改源代码,但还是有很多开发者在刚开始时会去这么做。在开发新项目时可以从官网下载一份全新的系统文件,然后比对下lib和app/code/core文件夹内的代码,看看核心代码有没有被修改。

Magento或者说PHP会在下面的目录中搜索类文件

lib/*
app/code/core/*
app/code/community/*
app/code/local/*

由于这个原因以及PHP构造时包含的顺序,在core/code/local中复制一个core文件系统就会先包含这个文件,所以如果想要修改产品模型的功能,就可以进行如下复制:

YOURS:    app/code/local/Mage/Catalog/Model/Product.php
ORIGINAL: app/code/core/Mage/Catalog/Model/Product.php

如果这里定义的是类而不是core文件,就无需包含核心文件,这也就避免了合并文件的麻烦,并且所有修改文件都会放在同一个目录结构中。这样虽然比直接修改核心源代码要好些,但还是有可能会修改掉一些重要方法,比如前面说到的产品模型中的getName方法

/**
     * Get product name
     *
     * @return string
     */
    public function getName()
    {
        return $this->_getData('name');
    }

在对这个方法进行重载时,我们有可能不小心添加了代码导致重载后返回空

/**
* LOCAL OVERRIDE! Get product name
*
* @return string
*/
public function getName($param=false)
{
    if($param == self:NICKNAME)
    {
        return $this->_getData('nickname');
    }
    else if($param == self::BAR)
    {
        return $this->_getData('name')
    }
 
    //forgot a return because we're working too hard
}

系统的其它部分可能会使用到这个方法并等待该方法返回一个字串,而我们的修改则会导致接下来的执行被中断。这里如果返回对象情况还会更糟,因为调用一个空方法会导致一个致命错误(Fatal)。

下面我们来看看同一模型中的validate方法

public function validate()
{
    Mage::dispatchEvent($this->_eventPrefix.'_validate_before', array($this->_eventObject=>$this));
    $this->_getResource()->validate($this);
    Mage::dispatchEvent($this->_eventPrefix.'_validate_after', array($this->_eventObject=>$this));
    return $this;
}

在修改时我们有可能不小心删除了dispatch事件

//My local override!
public function validate()
{    
    $this->_getResource()->validate($this);
    $this->myCustomValidation($this);
    return $this;
}

这时系统中其它依靠这一事件的部分都会停止运转。此外,在升级时同样存在着危险,如果升级中更新了类,而我们仍将使用旧的过时了的方法。也就是说我们还是需要在升级时进行手动的代码合并。

Magento中的类重载和系统重写依赖于创建模型、Helper和Block时使用的工厂模式,比如在执行如下代码时:

Mage::getModel('catalog/product');

实际是在告诉Magento去查找catalog/product用到的类并进行实例化。接下来,Magento会付出查看系统配置文件,并在config.xml中查看针对catalog/product应该使用哪个类,然后Magento就会对类进行实例化并返回一个模型。

而当我们在Magento中重载一个类时,实际上修改了配置文件,在进行上述操作时,就会告诉系统:如果要实例化catalog/product模型的话,请不要使用core中的类,而使用我们定义的类Myp_Mym_Model_Product

同时,在定义我们自己的类时,需要继承原来的类

class Myp_Mym_Model_Product extends Mage_Catalog_Model_Product
{
}

这样,我们的新类就会包含系统类中的功能,同时也就避免了在升级时还需要合并文件的功能,也不会在升级时包含过时的方法。但是在修改方法时还是存在问题,我们还是以getName和validate方法为例,我们还是会忘记返回值或返回错误的值,甚至是忘记添加方法中重要功能的代码块:

class Myp_Mym_Model_Product extends Mage_Catalog_Model_Product
{
    public function validate()
    {    
        $this->_getResource()->validate($this);
        $this->myCustomValidation($this);
        return $this;
    }  
 
    public function getName($param=false)
    {
        if($param == self:NICKNAME)
        {
            return $this->_getData('nickname');
        }
        else if($param == self::BAR)
        {
            return $this->_getData('name')
        }
 
        //forgot a return because we're working too hard
    }      
}

重载重写系统无法在这个层面上形成保护,但也提供了一些避免的方式。同于我们是继承了原来的类,因而可以在构造时使用parent::来调用原类中的方法:

class Myp_Mym_Model_Product extends Mage_Catalog_Model_Product
{
    public function validate()
    {  
        //put your custom validation up here
        return parent::validate();
    }  
 
    public function getName($param=false)
    {
        $original_return = parent::getName();
        if($param == self::SOMECONST)
        {
            $original_return = $this->getSomethingElse();
        }          
        return $original_return;
    }      
}

通过调用原来的方法,就可以确保该进行的操作都会完成,并且也减少了返回非系统要求值的可能性。当然只能是减少,而非彻底消除,开发者还是需要负责在自建代码中返回和原方法相同的对象或其它值。也就是,即使应用了重载系统,系统还是可能会由于误操作导致崩溃。

鉴于此,我们在进行开时,应使重载最小化,重载时可以在最后添加如下代码:

return parent::originalMethod();

如果重载时要求先运行原方法,可以使用如下方法:

public function someMethod()
{
    $original_return = Mage::getModel('mymodule/immutable')
    ->setValue('this is a test');
 
    //my custom code here
    return $original_return->getValue();
}

mymodule/immutable指向如下类

class Alanhou_Mymodule_Model_Immutable
{
    protected $_value=null;
    public function setValue($thing)
    {
        if(is_null($this->_value))
        {
            $this->_value = $thing;
            return $this;
        }
 
        //if we try to set the value again, throw an exception.
        throw new Exception('Already Set');
    }
 
    public function getValue()
    {
        return $this->_value;
    }
}

这样并不能保证$original_return不会被我们自己或其它人重写,但确实会在修改后更容易发现。如果在重载类的方法最后没有使用$original_return->getValue或parent::method,那么在debug时就很容易发现问题。

还有的时候我们会希望修改core方法中的返回值,在有这种需求时,定义一个新方法来调用原方法并在主题中调用新方法往往更为安全:

class Mage_Catalog_Model_Original extends Mage_Core_Model_Abstract
{
    protected function getSomeCollectionOriginal()
    {
        return Mage::getModel('foo/bar')
        ->getCollection()->addFieldToFilter('some_field', '42');
    }
}
 
class Myp_Mym_Model_New extends Mage_Catalog_Model_Original
{
    public function getSomeCollectionWithAdditionalItems()
    {
        $collection = $this->getSomeCollectionOriginal();
        //now, alter or override the $collection with
        //your custom code
 
        return $collection;
    }
}

这样可以保证原方法的一些功能依然有用,原来的返回类型保持不变,而我们也可以在系统中的某些地方添加自己的逻辑。

Magento开发系列之十 Varien数据集合

早期PHP开发者如果想要将一组相关的变量放在一起,只能通过数组,虽然和C语言的内存地址数组同名,PHP的数组更多的是一个对象加序列索引的数组行为组成的通用字库。在其它语言中都有多种数据结构,每种结构在存储、速度和语法上有不同的优势。PHP语言的逻辑是减去这些选择而仅使用一种有用的数据结构即可。从PHP 5开始通过提供内置类和接口来让开发者允许创建自己的数据结构。

$array = new ArrayObject();
class MyCollection extends ArrayObject{}
$collection = new MyCollection();
$collection[] = 'bar';

一些软件工程师肯定还不满足于此,因为没有获取到底层实现的细节,但已经获得包含特定功能的方法来创建类似数组的对象。也可以设定一定的安全规则来只允许某些类型的对象进行集合,Magento中包含一些这种集合,事实上每个遵循Magento接口的模型对象都会获得一个集合类型。要成为有效的Magento开发者,了解这些集合的工作机制也非常重要。本节我们就来看看Magento的集合,创建一个可以自由添加代码的控制器action。

第一步需要创建一些新的对象

$thing_1 = new Varien_Object();
$thing_1->setName('Richard');
$thing_1->setAge(24);

$thing_2 = new Varien_Object();
$thing_2->setName('Jane');
$thing_2->setAge(12);

$thing_3 = new Varien_Object();
$thing_3->setName('Spot');
$thing_3->setLastName('The Dog');
$thing_3->setAge(7);

Varien_Object类定义了所有Magento模型所继承的类,这面向对象的系统中这很常见,它可以保证可以轻易地为每个对象添加方法、功能而无需编辑每个类文件。每个继承Varien_Object的对象都会获得魔术方法getter和setter,这样就可以通过get和set来获取并设置数据。比如,

var_dump($thing_1->getName());

如果不知道要获取的属性名,可以将所有数据取出到一个数组中,如

var_dump($thing_3->getData());

上面的代码将返回下面这样的数组:

array
'name' => string 'Spot' (length=4)
'last_name' => string 'The Dog' (length=7)
'age' => int 7

你可能会注意到last_name这个属性,中间是由下划线来划分的,如果想要使用getter和setter魔术方法,需要采用驼峰法来进行命名:

$thing_1->setLastName('Smith');

在较新版本的Magento中,可以使用数组形式的中括号来取得属性值:

var_dump($thing_3["last_name"]);

这是PHP5中强大的功能,现在我们创建了一些对象,让我们一起来加入到集合中吧,要记得集合类似数组,但是是由PHP开发人员定义的:

$collection_of_things = new Varien_Data_Collection();
$collection_of_things
->addItem($thing_1)
->addItem($thing_2)
->addItem($thing_3);

Magento中的数据集合通常都继承自Varien_Data_Collection,所有Varien_Data_Collection中的方法都可以被调用,对于集合我们可以用foreach函数来进行遍历

foreach($collection_of_things as $thing)
{
    var_dump($thing->getData());
}

还可以通过以下方法取出集合中的第一项和最后一项:

var_dump($collection_of_things->getFirstItem());
var_dump($collection_of_things->getLastItem()->getData());

如果想要以XML形式获取集合中的数据,可以使用如下方法

var_dump( $collection_of_things->toXml() );

那如果只要取出指定的属性呢?

var_dump($collection_of_things->getColumnValues('name'));

Magento开发团队甚至还添加了过滤功能:

var_dump($collection_of_things->getItemsByColumnValue('name','Spot'));

为什么要特别地讲这些东西呢?因为所有的内置数据集合都继承自这个对象,也就是说可以对产品集合等进行同样的操作,比如:

public function testAction()
{
    $collection_of_products = Mage::getModel('catalog/product')->getCollection();
    var_dump($collection_of_products->getFirstItem()->getData());
}

Magento中大多数的模型对象都有一个getCollection方法,它可以实例化集合并返回所有系统中支持的对象类型。数据集合包含很多决定使用index还是缓存的复杂逻辑,还有针对EAV实体的一些逻辑。在同一个集合中连续多次调用同一方法可能会产生一些不可控的效果,因此下面所有的例子都是放在一个action方法中进行的,建议在独自测试时也这么做。另外var_dump对于对象和集合相当的有用,因为通常针对大规模递归的对象时,它也会很智能的显示出对象的结构。产品集合还有其它的一些Magento集合,还继承了Varien_Data_Collection_Db,这样就可以使用很有有用的方法,比如要查看集合的select语句:

public function testAction(){
	$collection_of_products = Mage::getModel('catalog/product')->getCollection();
	var_dump($collection_of_products->getSelect());
}

输出结果如下:

object(Varien_Db_Select)#86 (5) {
["_bind":protected]=> array(0) { }
["_adapter":protected]=> object(Magento_Db_Adapter_Pdo_Mysql)

看上去一头雾水,由于Magento使用了Zend数据抽象层,select也是对象,我们修改代码如下:

public function testAction(){
	$collection_of_products = Mage::getModel('catalog/product')->getCollection();
	// var_dump($collection_of_products->getSelect());
	var_dump((string) $collection_of_products->getSelect());
}

这次的输出结果可能是如下的这种简单SQL:

string(49) &quot;SELECT <code>e</code>.* FROM <code>catalog_product_entity</code> AS <code>e</code>&quot;

也有可能是更为复杂的语句:

string 'SELECT <code>e</code>.*, <code>price_index</code>.<code>price</code>, <code>price_index</code>.<code>final_price</code>, IF(<code>price_index</code>.<code>tier_price</code>, LEAST(<code>price_index</code>.<code>min_price</code>, <code>price_index</code>.<code>tier_price</code>), <code>price_index</code>.<code>min_price</code>) AS <code>minimal_price</code>, <code>price_index</code>.<code>min_price</code>, <code>price_index</code>.<code>max_price</code>, <code>price_index</code>.<code>tier_price</code> FROM <code>catalog_product_entity</code> AS <code>e</code>
INNER JOIN <code>catalog_product_index_price</code> AS <code>price_index</code> ON price_index.entity_id = e.entity_id AND price_index.website_id = '1' AND price_index.customer_group_id = 0'

这种区别主要取决于所select的属性,以及前所提到的index和cache的问题。如果学习过前面的章陈明,就知道Magento的很多模型包括产品模型使用EAV系统,默认情况下EAV集合不会包含所有对象属性,可以通过使用addAttributeToSelect方法来添加所有属性:

$collection_of_products = Mage::getModel('catalog/product')->getCollection()->addAttributeToSelect('*');

也可以只添加一个属性

$collection_of_products = Mage::getModel('catalog/product')->getCollection()->addAttributeToSelect('meta_title');

或者多个属性:

$collection_of_products = Mage::getModel('catalog/product')->getCollection()->addAttributeToSelect('meta_title')->addAttributeToSelect('price');

很多刚接触Magento ORM系统的PHP开发者总是搞不清楚该在什么时候进行数据库调用。在书写SQL语句甚至是使用基础的ORM系统时,SQL调用都在实体化对象当时发生。

$model = new Customer();
//SQL Calls being made to Populate the Object
echo 'Done'; //execution continues

但在Magento中并不是这样,它使用了延时加载(Lazy Loading)的概念。简单地说,延时加载是指只有在客户端程序需要获取数据时才会执行SQL调用,也就是如果进行如下操作:

$collection_of_products = Mage::getModel('catalog/product')->getCollection();

Magento并没有操作数据库,因此你可以在后面再添加属性:

$collection_of_products = Mage::getModel('catalog/product')->getCollection();
$collection_of_products->addAttributeToSelect('meta_title');

这里完全不用担心Magento会在每次添加属性时都去进行数据库查询,这些查询只有到了需要获取集合中的数据时才会进行。

数据库集合中最重要的一个方法是addFieldToFilter,这就添加了WHERE语句,可以使用自己的SKU也可以安装测试数据来进行如下测试:

public function testAction(){
	$collection_of_products = Mage::getModel('catalog/product')->getCollection();
	$collection_of_products->addFieldToFilter('sku','n2610');
	echo "Our collection now has ". count($collection_of_products). ' item(s)';
	var_dump($collection_of_products->getFirstItem()->getData());
}

addFieldToFilter中的第一个参数就是我们想要过滤的属性,而第二个则是查询的值。本例中我们添加了sku作为过滤,值为n2610。第二个参数也可以用于指定想要过滤的类型,这个有些复杂,不过值得更深入地研究一下,所以默认情况下:

$collection_of_products->addFieldToFilter('sku','n2610'); 

相当于

WHERE sku = "n2610"

接下来运行一下如下语句:

public function testAction(){
	var_dump((string) Mage::getModel('catalog/product')->getCollection()->addFieldToFilter('sku','n2610')->getSelect());
}

将会得到如下结果:

string(77) &quot;SELECT <code>e</code>.* FROM <code>catalog_product_entity</code> AS <code>e</code> WHERE (<code>e</code>.<code>sku</code> = 'n2610')&quot;

注意一旦我们使用了EAV属性,就会变得更为复杂,下面添加一个属性:

var_dump((string) Mage::getModel('catalog/product')->getCollection()->addAttributeToSelect('*')->addFieldToFilter('meta_title','my title')->getSelect());

输出会得到下面这样复杂的SQL语句:

string(670) &quot;SELECT <code>e</code>.*, IF(at_meta_title.value_id &gt; 0, at_meta_title.value, at_meta_title_default.value) AS <code>meta_title</code> FROM <code>catalog_product_entity</code> AS <code>e</code> INNER JOIN <code>catalog_product_entity_varchar</code> AS <code>at_meta_title_default</code> ON (<code>at_meta_title_default</code>.<code>entity_id</code> = <code>e</code>.<code>entity_id</code>) AND (<code>at_meta_title_default</code>.<code>attribute_id</code> = '82') AND <code>at_meta_title_default</code>.<code>store_id</code> = 0 LEFT JOIN <code>catalog_product_entity_varchar</code> AS <code>at_meta_title</code> ON (<code>at_meta_title</code>.<code>entity_id</code> = <code>e</code>.<code>entity_id</code>) AND (<code>at_meta_title</code>.<code>attribute_id</code> = '82') AND (<code>at_meta_title</code>.<code>store_id</code> = 1) WHERE (IF(at_meta_title.value_id &gt; 0, at_meta_title.value, at_meta_title_default.value) = 'my title')&quot;

那如果我们不只想要在查询中使用等号呢?比如大于、小于、不等于等等。addFieldToFilter中的第二个参数可以解决这一问题,在这里可以不传入一个字符串,而是传入一个数组。比如我们进行如下修改:

public function testAction(){
	var_dump((string) Mage::getModel('catalog/product')->getCollection()->addFieldToFilter('sku',array('eq'=>'n2610'))->getSelect());
}

其中的过滤即为:

addFieldToFilter('sku',array('eq'=>'n2610'))

可以看出,第二个参数是一个数组,键为eq,表示equals(等于),而值为n2610,即为想要过滤的值。Magento中有很多半英文的过滤符号,和perl中的基本一致,下面是一些过滤方式及其对应的SQL语句:

array("eq"=>'n2610')
WHERE (e.sku = 'n2610')
 
array("neq"=>'n2610')
WHERE (e.sku != 'n2610')
 
array("like"=>'n2610')
WHERE (e.sku like 'n2610')
 
array("nlike"=>'n2610')
WHERE (e.sku not like 'n2610')
 
array("is"=>'n2610')
WHERE (e.sku is 'n2610')
 
array("in"=>array('n2610'))
WHERE (e.sku in ('n2610'))
 
array("nin"=>array('n2610'))
WHERE (e.sku not in ('n2610'))
 
array("notnull"=>'n2610')
WHERE (e.sku is NOT NULL)
 
array("null"=>'n2610')
WHERE (e.sku is NULL)
 
array("gt"=>'n2610')
WHERE (e.sku > 'n2610')
 
array("lt"=>'n2610')
WHERE (e.sku &lt; 'n2610')
 
array("gteq"=>'n2610')
WHERE (e.sku >= 'n2610')
 
array("moreq"=>'n2610') //a weird, second way to do greater than equal
WHERE (e.sku >= 'n2610')
 
array("lteq"=>'n2610')
WHERE (e.sku &lt;= 'n2610')
 
array("finset"=>array('n2610'))
WHERE (find_in_set('n2610',e.sku))
 
array('from'=>'10','to'=>'20')
WHERE e.sku >= '10' and e.sku &lt;= '20

基本上一看就能够明白,这里挑出几个特别的解释一下

in, nin

in和nin条件语句中可以传入一组值,这里值的部分也可以使用数组。

array("in"=>array('n2610','ABC123')
WHERE (e.sku in ('n2610','ABC123'))

notnull, null

NULL类型在SQL中非常特别,不能通过等号运算符来过滤,通常使用null或notnull即可进行过滤,后面传入的值会被忽略掉

array("notnull"=>'n2610')
WHERE (e.sku is NOT NULL)

from – to

这是另外一个各其它语句不一样的格式,它传入的不是只有一对键值的数组,而是拥有两对键值,一个用于from,另一个用于to。从键名就可以猜它是用于限定一个范围的:

public function testAction
{
        var_dump( (string) Mage::getModel('catalog/product')->getCollection()->addFieldToFilter('price',array('from'=>'10','to'=>'20'))->getSelect());                      
}

将会生成如下的SQL语句:

WHERE (_table_price.value >= '10' and _table_price.value <= '20')

最后我们来看看布尔值操作符,我们很少会只过滤一个属性,Magento自然早已考虑到这一情况。可以通过调用多个addFiledToFilter生成AND查询

function testAction()
{
        echo( (string) Mage::getModel('catalog/product') ->getCollection()->addFieldToFilter('sku',array('like'=>'a%'))->addFieldToFilter('sku',array('like'=>'b%'))->getSelect());
}

将生成如下SQL语句:

SELECT <code>e</code>.* FROM <code>catalog_product_entity</code> AS <code>e</code> WHERE (<code>e</code>.<code>sku</code> LIKE 'a%') AND (<code>e</code>.<code>sku</code> LIKE 'b%')

可以发现以上WHERE语句中有一个AND,当然,这条语句不会返回任何,因为sku不可能同时以a和b开头。那这种情况下我们应用使用OR语句,要这么做需要在addFieldToFilter的第二个参数处传一个过滤数组,通过定义变量来做为数组可能更为直观:

$filter_a = array('like'=>'a%');
$filter_b = array('like'=>'b%');

那么最终我们将使用如下方法:

public function testAction()
{
        $filter_a = array('like'=>'a%');
        $filter_b = array('like'=>'b%');
        echo((string) Mage::getModel('catalog/product')->getCollection()->addFieldToFilter('sku',array($filter_a,$filter_b))->getSelect());
}

这次我们将得到如下的SQL语句:

SELECT <code>e</code>.* FROM <code>catalog_product_entity</code> AS <code>e</code> WHERE (((<code>e</code>.<code>sku</code> LIKE 'a%') OR (<code>e</code>.<code>sku</code> LIKE 'b%')))

 

Magento开发系列之九 后台开发进阶

前面一节我们讨论了后台系统配置,本节我们将进行更深入的探讨,前面讲到了field内可以用到的一些标签:

<fields>
    <!-- ... --->
    <fieldname translate="label">
        <label>Field Name</label>
        <frontend_type>text</frontend_type>
        <sort_order>2</sort_order>
        <show_in_default>1</show_in_default>
        <show_in_website>1</show_in_website>
        <show_in_store>0</show_in_store>
    </fieldname>
    <!-- ... --->
</fields>

本节将对这些标签进行更深度的剖析,首先查看<label /><comment />标签,<label />标签中的内容将出现在文本框左侧,而<comment />标签则出现在文本框的下方

<fields>
	<demo_text>
		<label>This is a label</label>
		<frontend_type>text</frontend_type>
		<comment>While this is a comment</comment>
		<show_in_default>1</show_in_default>
		<show_in_website>1</show_in_website>
		<show_in_store>1</show_in_store>
	</demo_text>
</fields>

<labe />和<comment />标签

<show_in_default />, <show_in_website />,和<show_in_store />

这几个标签内都使用布尔型的0或1,用于定义在defautl视图、website视图或store视图中是否显示所添加的field。同时通过这一配置也可以控制选项在哪个视图里是可编辑的,比如可以设定在store视图里不可用:

<show_in_store>0</show_in_store>

当然通过编辑还可以在store社图级别设定或获取其中的值,但我们不推荐这么去做。

<sort_order />

<sort_order />中传入的是数值,用于决定在分组中field的排序,数值越大,则会出现在越下面。

<frontend_type />

<frontend_type />用于定义在配置中field的类型,所支持的类型有:

  1. allowspecific
  2. export
  3. image
  4. import
  5. label
  6. multiselect
  7. obscure
  8. password
  9. select
  10. text
  11. textarea
  12. time

这些值会按照工厂模式实例如下格式的类:

Varien_Data_Form_Element_Type

其中的Type对应的就是<frontend_type />中的值,这个动作是在Varien_Data_Form_Abstract(lib/Varien/Data/Form/Abstract.php)类中addField方法内进行的

class Varien_Data_Form_Abstract extends Varien_Object
{
... ...
public function addField($elementId, $type, $config, $after=false)
    {
        if (isset($this->_types[$type])) {
            $className = $this->_types[$type];
        }
        else {
            $className = 'Varien_Data_Form_Element_'.ucfirst(strtolower($type));
        }
        $element = new $className($config);
        $element->setId($elementId);
        $this->addElement($element, $after);
        return $element;
    }
... ...
}

<frontend_class />

<frontend_class />不是一个类群的名称,该标签可用于修改给field生成的表单元素标签的class属性,也就是说可以通过这个配置可以为表单属性添加一个CSS的类。比如:

<frontend_type>select</frontend_type>
<frontend_class>free-method</frontend_class>

会在结果页面变成:

<select class="free-method">

<validate />

<validate />这个标签看上去会让人误解,它只是在标签中添加一个CSS类

<frontend_type>text</frontend_type>
<validate>validate-email</validate>

上述代码会在结果页面变成:

<input class="validate-email">

Magento中还有更多的配置,这个CSS会在客户端触发验证机制,比如上面的代码会调用javascript来对表单的输入内容进行email验证。如果验证失败的话,就无法完成配置表单的提交。可以在以下文件中查看验证规则:

js/prototype/validation.js

email验证规则如下:

['validate-email', 'Please enter a valid email address. For example johndoe@domain.com.', function (v) {
                return Validation.get('IsEmpty').test(v) || /^([a-z0-9,!\#\$%&amp;'\*\+\/=\?\^_<code>\{\|\}~-]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z0-9,!\#\$%&amp;'\*\+\/=\?\^_</code>\{\|\}~-]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*@([a-z0-9-]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z0-9-]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*\.(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]){2,})$/i.test(v)
            }],

这是一条功能很强大的正则表达式。

<can_be_empty />

<can_be_empty />标签针对多选框,也就是在提交时允许不选择任何选项。在设置can_be_empty为真时,实际上是在系统配置页面添加了一个hidden的input标签(文件地址:/lib/Varien/Data/Form/Element/Multiselect.php):

if ($this->getCanBeEmpty() && empty($this->_data['disabled'])) {
	$html .= '<input type="hidden" name="' . parent::getName() . '" value="" />';
}

这样在执行的时候就可以允许不予选择。

<depends />

<depends />标签用于设置仅在本组内其它配置项为指定值时显示某一配置项。比如,PayPal Express系统配置中有有如下的定义:

<specificcountry translate="label">
    <label>Countries Payment Applicable From</label>
    <frontend_type>multiselect</frontend_type>
    <sort_order>110</sort_order>
    <source_model>adminhtml/system_config_source_country</source_model>
    <show_in_default>1</show_in_default>
    <show_in_website>1</show_in_website>
    <show_in_store>0</show_in_store>
    <depends><allowspecific>1</allowspecific></depends>
</specificcountry>

上述代码中有一段

<depends><allowspecific>1</allowspecific></depends>

<allowspecific />在以下代码中定义:

<allowspecific translate="label">
   <label>Payment Applicable From</label>
   <frontend_type>select</frontend_type>
   <sort_order>100</sort_order>    <source_model>adminhtml/system_config_source_payment_allspecificcountries</source_model>
   <show_in_default>1</show_in_default>
   <show_in_website>1</show_in_website>
   <show_in_store>0</show_in_store>
</allowspecific>

如果<allowspecific>选项中的值为1,就会显示<specificcountry>版块,这个是通过后台的JS来即时实现的。虽然所有的表单项都可以使用onchange事件,但在Magento中只针对父级选项是select时才使用这一功能。

<source_model />

<source_model />标签通过以UR/类群名称格式指定模型类来设置field的默认选项,它可以与select和multi-select一起使用。除了标准的类群命名,还可以采用扩展形式:

module/modelname::methodName

系统会使用getModel(‘module/modulename’)来实例化模型,然后调用methodName来获取值-标签对来作为数据源。假如没有添加methodName,默认会调用toOptionArray方法。

<frontend_model />

默认情况下,Magento的表单元素使用Block类来进行处理

Mage_Adminhtml_Block_System_Config_Form_Field

但是如果想要自己指定系统配置选项的处理文件时,可以通过<frontend_model />标签以URI/类群名称的方式来指定另一个block类。比如针对adminnotification组中的<last_update />项可以进行如下配置:

<last_update translate="label">
    <label>Last update</label>
    <frontend_type>label</frontend_type>
    <frontend_model>adminhtml/system_config_form_field_notification</frontend_model>
    <sort_order>3</sort_order>
    <show_in_default>1</show_in_default>
    <show_in_website>0</show_in_website>
    <show_in_store>0</show_in_store>
</last_update>

这里就指定了system_config_form_field_notification来进行处理,对应的类就是app/code/core/Mage/Adminhtml/Block/System/Config/Form/Field/Notification.php文件中的如下类

class Mage_Adminhtml_Block_System_Config_Form_Field_Notification extends Mage_Adminhtml_Block_System_Config_Form_Field
{
    protected function _getElementHtml(Varien_Data_Form_Element_Abstract $element)
    {
        $element->setValue(Mage::app()->loadCache('admin_notifications_lastcheck'));
        $format = Mage::app()->getLocale()->getDateTimeFormat(Mage_Core_Model_Locale::FORMAT_TYPE_MEDIUM);
        return Mage::app()->getLocale()->date(intval($element->getValue()))->toString($format);
    }
}

这里通过重载_getElementHtml方法确保所输入日期会以相同格式进行显示。

<backend_model />

一旦在Magento中进行表单提交,数据就会被存储起来,对于系统配置项,通常是由以下的模型类来进行处理的

 Mage_Core_Model_Config_Data

但在开发中经常会想要使用其它的后台模型,同样地,可以通过<backend_model />标签以URI/类群名称的方式来指定另一个模型类。通常这样做不是因为想要修改数据存储地址,而是想在field保存时进行一些其它的操作。通过自建模型继承Mage_Core_Model_Config_Data类并在模型中定义_beforeSave和_afterSave方法,可以在配置值发生更改时进行其它的操作。

以tablerate组内的import项为例

<import translate="label">
    <label>Import</label>
    <frontend_type>import</frontend_type>
    <backend_model>adminhtml/system_config_backend_shipping_tablerate</backend_model>
    <sort_order>6</sort_order>
    <show_in_default>0</show_in_default>
    <show_in_website>1</show_in_website>
    <show_in_store>0</show_in_store>
</import>

类群名adminhtml/system_config_backend_shipping_tablerate表示app/code/core/Mage/Adminhtml/Model/System/Config/Backend/Shipping/Tablerate.php文件中下面这个类

class Mage_Adminhtml_Model_System_Config_Backend_Shipping_Tablerate extends Mage_Core_Model_Config_Data
{
    public function _afterSave()
    {
        Mage::getResourceModel('shipping/carrier_tablerate')->uploadAndImport($this);
    }
}

这里通过在模型保存后调用的_afterSave方法,将刚刚上传的文件信息来更新shipping/carrier_tablerate模型。

<upload_dir />和<base_url />

这两个标签都用于拥有<backend_model>adminhtml/system_config_backend_image</backend_model>后台模型的<frontend_type>image</frontend_type>项。用于指定上传图片存储地址以及在<img>标签中调取图片基础的网址路径。

我们先来说说<upload_dir />标签

<upload_dir config="system/filesystem/media" scope_info="1">sales/store/logo</upload_dir>

上面的代码指定了三个内容

  1. 上传图片的基本路径
  2. 相对基本路径这个图片项上传的路径
  3. 当前配置域是否添加到路径之中

上传图片基本路径通过config属性指定,即为system/filesystem/media,这个指定的是系统配置路径,也就是说图片上传的路径不是system/filesystem/media,而是安装Magento后系统system/filesystem/media配置值,即{{root_dir}}/media。

一旦找到了上传图片的基本路径,需要在其后添加子目录来指定图片传的地址,这是通过<upload_dir />中的值来指定的,这里是sales/store/logo,添加完成后整个路径即为:

/path/to/magento/install/media/sales/store/logo

如果scope属性值为1,则当前配置域会转变为路径,假设将图片上传到default域中,上传路径即为:

/path/to/magento/install/media/sales/store/logo/default

而如果在某一个store中上传图片的话,就会得到类似下面这样的路径:

/path/to/magento/install/media/sales/store/logo/store/5

在上传图片时,只有路径中的作用域(scope)和图片名称会保存到配置中,也就是说我们需要指定图片的基础url

<base_url type="media" scope_info="1">sales/store/logo</base_url>

scope_info和文本节点与<upload_dir />相同,<base_url />的不同之处在于它设置的是图片网址的基础地址。你可能已经猜到,基础路径通过type属性来进行设置,其值会通过全局Mage对象传入getBaseUrl方法来设定图片的基础路径。上例中如得到如下这样的调用:

Mage::getBaseUrl('media')

实际代码可以查看在app/code/core/Mage/Adminhtml/Block/System/Config/Form/Field/Image.php中的如下类:

class Mage_Adminhtml_Block_System_Config_Form_Field_Image extends Varien_Data_Form_Element_Image
{

    /**
     * Get image preview url
     *
     * @return string
     */
    protected function _getUrl()
    {
        $url = parent::_getUrl();

        $config = $this->getFieldConfig();
        /* @var $config Varien_Simplexml_Element */
        if (!empty($config->base_url)) {
            $el = $config->descend('base_url');
            $urlType = empty($el['type']) ? 'link' : (string)$el['type'];
            $url = Mage::getBaseUrl($urlType) . (string)$config->base_url . '/' . $url;
        }

        return $url;
    }

}

需要注意的是这个基础地址并不是在Magento中各处用于生成配置过图片的完整路径,应该只是作用于后台配置中的图片预览。