Magento 产品页如何使用外部链接

Magento 无疑是电商中数一数二的系统,Alan 不从事跨境电商行业的工作已经有三年了,因而对于 Magento也逐渐生疏了(话说它也An eBay Company 变成了An Adobe Company)。趁着中秋假日做了些梳理,产生了一个小需求,就是我们站点中除了销售自己的产品外,还常常会需要链接接其它网站,这种需求最常见于 Affliate Marketing。

首先进入后台 Catalog > Attributes > Manage Attributes(Magento 2 Stores > Attributes > Product),点击 Add New Attribute 按钮添加属性,如 external_link,在Manage Label / Options中输入名称 External Link 点击保存

Magento 添加属性

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文件中进行修改

30天备战Magento认证考试二

Day 2 Continued

Explain how Magento loads and manipulates configuration information

Magento的配置基本上散布在很多个.xml文件中,那么很自然地就有一个疑问,Magento是如何操作这些文件又从而为每个插件找到相应的配置文件的呢?下面我们先来梳理一下Magento结构的核心要点:

• Magento是一个模块化系统,功能分布在各个模块中;

• 有三个代码池,分别是local, community和core;

• 每个模块中包含app/code/[codePool]/Namespace/Modulename/config.xml (该文件 中存放基本模块配置)和app/etc/modules/Namespace_Modulename.xml (该文件存放代码池信息以及插件激活标记);

• Magento安装时的全局配置(包括数据库连接系统和后台地址)存放在app/etc/config.xml和app/etc/local.xml文件中;

如是我们从index.php开始追踪代码的话,会看到如下内容(在1.9.*的index.php中未找到这部分内容,使用的是Mage::run($mageRunCode, $mageRunType)):

Mage::run()
        self::$_config = new Mage_Core_Model_Config($options); (in CE 1.6.* and previous versions)
        self::_setConfigModel($options); (in СE 1.7.*)
        self::$_app    = new Mage_Core_Model_App();
        self::$_app->run(…);

或者

Mage::app()
        self::$_app    = new Mage_Core_Model_App();
        self::$_app-> init (…);

之后在两个版本中都有相似的类方法加载顺序。在Mage::run()中所有处理配置加载的内容都在Mage_Core_Model_App中并引用Mage_Core_Model_Config方法。调用Mage::app()会立即调用Mage_Core_Model_Config::init(),其中包含配置加载的进程。
最后会到达Mage_Core_Model_Config, 继承自Mage_Core_Model_Config_Base和Varien_Simplexml_Config。在这个层面调用方法无关紧要,让我们一起来看看Mage_Core_Model_Config:: init()方法(app/code/core/Mage/Core/Model/Config.php):

<?php /** * Initialization of core configuration * * @return Mage_Core_Model_Config */ public function init($options=array()) { // lets skip cache init and non-standard options stuff $this->loadBase();
        $cacheLoad = $this->loadModulesCache();
        if ($cacheLoad) {
            return $this;
        }
        $this->loadModules();
        $this->loadDb();
        $this->saveCache();
        return $this;
    }

让我们来逐行分析一下这个方法吧

$this->loadBase();

    /**
     * Load base system configuration (config.xml and local.xml files)
     *
     * @return Mage_Core_Model_Config
     */
    public function loadBase()
    {
        $etcDir = $this->getOptions()->getEtcDir();
        $files = glob($etcDir.DS.'*.xml');
        $this->loadFile(current($files));
        while ($file = next($files)) {
            $merge = clone $this->_prototype;
            $merge->loadFile($file);
            $this->extend($merge);
        }
        if (in_array($etcDir.DS.'local.xml', $files)) {
            $this->_isLocalConfigLoaded = true;
        }
        return $this;
    }

在最前面定义了一具app/etc目录的绝对路径,然后获取了此目录下的.xml文件列表,读取其中的内容并并入一个simpleXmlElement对象中。如果local.xml文件被载入了的话(也就意味着Magento已经完成了安装),则设置$this->_isLocalConfigLoaded = true;这会在后面用于实例化店铺并加载模块设置脚本。

只要这个方法不限制名称和载入文件数量,我们就可以在需要时载入app/etc中的自定义.xml文件。在开发服务器上不通过local.xml文件来指定数据库连接信息会比较有帮助,只使用该文件包含生产服务器的信息

<?php $cacheLoad = $this->loadModulesCache();
        if ($cacheLoad) {
            return $this;
        }

这部分代码不言自明,如果配置中激活了缓存并包含所要求的内容,就加载整个配置。我们还会用这一配置替换app/etc中已载入的配置,从缓存中载入(Mage_Core_Model_Config:: loadCache())并返回到Mage_Core_Model_App。

您可能会问如果所有的配置都能从缓存中载入,为什么不在扫描app/etc目录前就进和这一验证呢?因为Magento的缓存不仅可作为文件存放在var/cache中,也可以存放在apc, memcached和xcache中。从app/etc中载入的步骤允许设定用于存储缓存的类型和配置。

接下来是加载最为广泛的配置部分-模块配置。

$this->loadModules();

    /**
     * Load modules configuration
     *
     * @return Mage_Core_Model_Config
     */
    public function loadModules()
    {
        Varien_Profiler::start('config/load-modules');
        $this->_loadDeclaredModules();
 
        $resourceConfig = sprintf('config.%s.xml', $this->_getResourceConnectionModel('core'));
        $this->loadModulesConfiguration(array('config.xml',$resourceConfig), $this);
 
        /**
         * Prevent local.xml directives overwriting
         */
        $mergeConfig = clone $this->_prototype;
        $this->_isLocalConfigLoaded = $mergeConfig->loadFile($this->getOptions()->getEtcDir().DS.'local.xml');
        if ($this->_isLocalConfigLoaded) {
            $this->extend($mergeConfig);
        }
 
        $this->applyExtends();
        Varien_Profiler::stop('config/load-modules');
        return $this;
    }

Day 3

$this->_loadDeclaredModules();

_getDeclaredModuleFiles(): 首先扫描app/etc/modules目录获取指向系统所有模块的.xml文件路径列表。建立一个以base, mage和custom为键名的关联数组,仅有Mage_All.xml路径指向base版块,而Magento的基础包(core代码池app/code/core/Mage下)指向mage版块,custom版块涵盖其它的模块。最终会将所有内容并入一个数组,由于前面按键名拆分,最终数据的存储顺序是: Mage_All.xml, Mage命名空间里的模块,其它所有模块。

如果还没有明白Mage_All.xml的重要性的话,现在应该查看其中的内部结构了。Mage_All.xml包含了所有保证系统正常运行需要加载的模块。

然后collected.xml文件会加载到Mage_Core_Model_Config_Base $unsortedConfig。就会获得一个$moduleDepends数组,它是基于<depends>, <active>标记和模块名称。

$this->_sortModuleDepends($moduleDepends)在查看现存模块间的依赖性时会用到,在验证后会形成一个按照模块依赖关系排序的新的数组。

在_loadDeclaredModules()的最后会再次创建一个simpleXmlElement对象,并合并入前面创建的app/etc/*.xml中。

下面我们再加到loadModules()方法中:

<?php $resourceConfig = sprintf('config.%s.xml', $this->_getResourceConnectionModel('core'));
        $this->loadModulesConfiguration(array('config.xml',$resourceConfig), $this);

$resourceConfig中包含 config.mysql4.xml内容,因而下一步会载入config.mysql4.xml的配置以及config.mysql4.xml文件本身,目前尚不清楚config.mysql4.xml具体有什么作用。

这个方法首先会检测是否可以从local代码池中加入模块(注意app/etc/local.xml中false的值是否为true)。如果只允许使用community和core中的模块,Magento也会相应地修改include_path()。

<?php
        if ($disableLocalModules && !defined('COMPILER_INCLUDE_PATH')) {
            set_include_path(
                // excluded '/app/code/local'
                BP . DS . 'app' . DS . 'code' . DS . 'community' . PS .
                BP . DS . 'app' . DS . 'code' . DS . 'core' . PS .
                BP . DS . 'lib' . PS .
                Mage::registry('original_include_path')
            );
        }

通过这个简短的验证,Magento确保有一些simpleXmlElement设置(没有的话Magento会自动创建),然后会删除载入的配置为false和local的模块。
接着config.xml中剩余的模块以及config.mysql4.xml文件会被加载,然后loaded .xml会被加入到现有的文件中。如果最后加载的文件包含老的xpath, 系统会使用最后的值。

以前用$this->applyExtends();的调用配合自定义插件来改变/重载配置中的数据,但后续版本中并未采用。如果您已看到这部分的话,请仔细阅读一下下面的代码吧:

<?php /** * Prevent local.xml directives overwriting */ $mergeConfig = clone $this->_prototype;
        $this->_isLocalConfigLoaded = $mergeConfig->loadFile($this->getOptions()->getEtcDir().DS.'local.xml');
        if ($this->_isLocalConfigLoaded) {
            $this->extend($mergeConfig);
        }

不操作模块的话可以通过修改app/etc/local.xml中的数据,最后,调用$this->loadDb();方法

<?php /** * Load config data from DB * * @return Mage_Core_Model_Config */ public function loadDb() { if ($this->_isLocalConfigLoaded && Mage::isInstalled()) {
            Varien_Profiler::start('config/load-db');
            $dbConf = $this->getResourceModel();
            $dbConf->loadToXml($this);
            Varien_Profiler::stop('config/load-db');
        }
        return $this;
    }

这个方法使用了和core_config_data table相关的Mage_Core_Model_Resource_Config model模型资源,在这步我们从core_config_data载入数据到配置中:

1. 向站点中加入数据(查看core_website table)
2. 为现有站点加入各商店(store)数据(查看core_store table)
3. 根据域(scope)向core_config_data中加入数据
a. 先创建<default>块
b. 再创建<websites>块
c. 最后创建<stores>块
d. 每次迭代把域中的基本数据替换成更具针对性的数据 (还记得后台中的«use default», «use website»和«configuration scope» 吗?)
4. 自身配置 (如果遇到与现有网站不相关的数据,会进行删除)。

紧接着如果使用了缓存的话就会向缓存中写入配置。现在配置已经生成,但除了编译.xml文件外,我们还经常要从中读取数据,那么让我们回到Mage.php中查看相关方法:

<?php /** * Retrieve config value for store by path * * @param string $path * @param mixed $store * @return mixed */ public static function getStoreConfig($path, $store = null) { return self::app()->getStore($store)->getConfig($path);
    }
 
    /**
     * Retrieve config flag for store by path
     *
     * @param string $path
     * @param mixed $store
     * @return bool
     */
    public static function getStoreConfigFlag($path, $store = null)
    {
        $flag = strtolower(self::getStoreConfig($path, $store));
        if (!empty($flag) && 'false' !== $flag) {
            return true;
        } else {
            return false;
        }
    }

两个方法的唯一差别是 getStoreConfig() 会返回具体的值,而getStoreConfigFlag() 仅返回布尔值true或false。两个方法最终都调用Mage_Core_Model_Store::getConfig()方法:

<?php /** * Retrieve store configuration data * * @param string $path * @return string|null */ public function getConfig($path) { if (isset($this->_configCache[$path])) {
            return $this->_configCache[$path];
        }
 
        $config = Mage::getConfig();
 
        $fullPath = 'stores/' . $this->getCode() . '/' . $path;
        $data = $config->getNode($fullPath);
        if (!$data && !Mage::isInstalled()) {
            $data = $config->getNode('default/' . $path);
        }
        if (!$data) {
            return null;
        }
        return $this->_processConfigValue($fullPath, $path, $data);
    }

如果请求的信息无法在本地缓存中找到,该方法会使用stores/[store code]/[requested path]路径查找,如果依然没有结果,会使用另一个路径default/[requested path]在加载的配置中搜索。如果未查找到,则返回null。

查找到的数据由_processConfigValue()方法来进一步处理:

• 如果返回的是子节点,数据会循环存入本地缓存中。用户在当前会话再次调用时无需重新到配置中查找数据。
• 如果节点包含backend_model, 这个模型应按照指定格式获取数据。
• {{unsecure_base_url}}, {{unsecure_base_url}}, {{base_url}}的变量将由对应的数据进行替换。

最后总结一下

1. 所有.xml文件会汇集到一个大的simpleXmlElement 对象中

2. 数据首先从app/etc/*.xml中载入然后是app/etc/modules/*.xml。基于需加载模块的信息,会载入各模块etc目录下的config.xml。如果载入后台检测ACL权限并创建菜单元素,同时还会加载adminhtml.xmlsystem.xml。最后一步才是从数据库中载入配置数据。

3. 除开app/etc/local.xml中的其它所有参数都可以在自建模块的config.xml中进行重载。

P.S. 虽然Magento中提供了便捷的Mage:: getStoreConfig()Mage:: getStoreConfigFlag()方法,我们还是可以通过Mage::getConfig()->getNode($path, $scope, $scopeCode);来获取配置树中的任意元素。

可通过在根目录下运行如下代码来查看Magento安装后的最终配置信息:

<?php header("Content-Type: text/xml"); define('MAGENTO_ROOT', getcwd()); $mageFilename = MAGENTO_ROOT . '/app/Mage.php'; require_once $mageFilename; umask(0); /* Store or website code */ $mageRunCode = isset($_SERVER['MAGE_RUN_CODE']) ? $_SERVER['MAGE_RUN_CODE'] : ''; /* Run store or run website */ $mageRunType = isset($_SERVER['MAGE_RUN_TYPE']) ? $_SERVER['MAGE_RUN_TYPE'] : 'store'; Mage::app($mageRunCode, $mageRunType); $config = Mage::getConfig()->getNode()->asXml();
//file_put_contents('config.xml', $config);
echo $config;

Describe class group configuration and use in factory methods

Magento通过工厂方法来实例化..

 

注:考虑到时间的紧迫性,决定内容在后续补入,目前仅录入学习进度,上班族伤不起啊!!!

Describe the process and configuration of class overrides in Magento

Register an Observer

Identify the function and proper use of automatically available events, including *_load_after, etc.

Day 4

Set up a cron job

Describe how to plan for internationalization of a Magento site

Describe the use of Magento translate classes and translate files

Describe the advantages and disadvantages of using subdomains and subdirectories in internationalization

Day 5

Describe the steps for application initialization

Describe the role of the system entrypoint, index.php

Describe the role of the front controller

Identify uses for events fired in the front controller

Day 6

Describe URL structure/processing in Magento

Describe the URL rewrite process

Describe request routing/request flow in Magento

Describe how Magento determines which controller to use and how to customize route-to-controller resolution

Day 7

Describe the steps needed to create and register a new module

Describe the effect of module dependencies

Describe different types of configuration files and the priorities of their loading

Identify the steps in the request flow in which

1. Design data is populated

2. Layout configuration files are parsed

3. Layout is compiled

4. Output is rendered

30天备战Magento认证考试一

Day 1

使用Magento有挺长一段时间,但一直都限于泛泛的一些见招拆招的小功能,之前也参照Alan Storm的博客写过有关Magento开发的系列教程,不过在开发上实在没有什么更进一步的发展。于是决定通过备考Magento认证考试来进一步了解Magento的结构,从业余向专业过渡。

Magento开发系列之一 基础知识

Magento开发系列之二 配置文件

Magento开发系列之三 控制器

Magento开发系列之四 布局、块和模板

Magento开发系列之五 模型和ORM基础

Magento开发系列之六 安装、升级脚本

Magento开发系列之七 EAV-更高级的ORM

Magento开发系列之八 后台配置开发

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

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

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

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

这次的计划是在年底前参加考试,大约一个月的准备时间,于是有了30天备战Magento认证考试的标题,难度还是挺大的,不管成功或者失败,都希望在本文中进行真实的记录为更多参加Magento认证考试的人们所参考。

由于一直没有机会接触Magento的Enterprise版,所以本次计划参加的考试为Magento Certified Developer Exam而不是Plus,官方的大纲如下:
链接: http://pan.baidu.com/s/1jGCnR8a 密码: sw6u

Basics

Describe Magento codepools

Describe Magento codepools

代码池在Magento根目录的app/code目录下,通常有系统自带的core代码池(可以拷贝到local文件夹中再进行相应的更改),通常不建议直接修改core代码池中的代码;第三方开发、共享插件(如Magento Connect)等使用的community代码池以及本地开发的local代码池。

那么系统是如何与各代码池进行交互的呢?

可以查看一下app/Mage.php文件中如下代码:

/**
 * Set include path
 */
$paths = array();
$paths[] = BP . DS . 'app' . DS . 'code' . DS . 'local';
$paths[] = BP . DS . 'app' . DS . 'code' . DS . 'community';
$paths[] = BP . DS . 'app' . DS . 'code' . DS . 'core';
$paths[] = BP . DS . 'lib';

$appPath = implode(PS, $paths);
set_include_path($appPath . PS . Mage::registry('original_include_path'));
include_once "Mage/Core/functions.php";
include_once "Varien/Autoload.php";

从这段代码中我们可以看出Magento中的加载顺序,即先是local代码池,然后community代码池,最后才是core代码池,也正是因为如此,我们可以在开发时通过重载来修改core中的类。

Describe typical Magento module structure

在Magento中通常一个模块会包含 Controller, Model, Helper, Block等目录,比如app/code/core/Mage/Checkout模板下的文件夹结构:

Magento Checkout模块下面我就来一一了解Magento中的模块组成

Block
Block文件夹对应MVC中的View,块文件通过模板文件调整模型,该目录下的文件载入数据库的数据并传给主题中的.phtml模板文件。
Controllers
控制器代表给定请求(如dispatch(), preDispatch(), postDispatch()等方法)的所有业务逻辑动作,并向系统其它部分传递命令。

etc
etc文件夹中包含所有声明和配置模块行为的xml文件,每个模块都至少要包含一个config.xml文件,在这里可以声明所有的model, router, block和helper等,该文件类似:

<config>
	<modules>
		<Namespace_Modulename>
			<version>0.1.0</version>
		</Namespace_Modulename>
	</modules>
</config>

Helper
Helper包含一些在整个系统常用到的一些工具方法,在helper中声明的方法,可以在模板文件或block, model, controller类中通过如下方式调用

Mage::helper('modulename/helpername')->methodName();

每个模块都有一个默认的Data帮助类(Modulename/Helper/Data.php),可以通过如下方式调用:

Mage::helper('modulename')->methodName();

 

Model
在传统的MVC中,Model用于连接数据库并从中读取或写入数据。在Magento中却并非如此,刚开始用时可能会不明就里,官方如是说:

Most Magento Models can categorized in one of two ways. There’s a basic, ActiveRecord-like/one-object-one-table Model, and there’s also an Entity Attribute Value (EAV) Model. Each Model also gets a Model Collection. Collections are PHP objects used to hold a number of individual Magento Model instances. The Magento team has implemented the PHP Standard Library interfaces of IteratorAggregate and Countable to allow each Model type to have it’s own collection type. If you’re not familiar with the PHP Standard Library, think of Model Collections as arrays that also have methods attached. Magento Models don’t contain any code for connecting to the database. Instead, each Model uses two modelResource classes, (one read, one write), that are used to communicate with the database server (via read and write adapter objects).

sql
处理所有在模板中使用的自定义数据表以及所有插件的升级。

etc/modules/Namespace_Modulename.xml

想要知道应使用哪些模块以及它们的位置,需要在所有xml文件的etc/modules/文件夹中创建一个xml文件

<?xml version="1.0"?>
<config>
    <modules>
        <Namespace_Modulename>
            <active>true</active>
            <codePool>local</codePool>
        </Namespace_Modulename>
    </modules>
</config>

还可以在模块config.xml中指定depends, version和platform等参数:

Depends

<depends>
    <Mage_Catalog />
</depends>

Version

<version>0.1.0</version>

Platform

<platform>сe</platform> <!-- (ce=community edition,pe=professional etc.) -->

Day 2

Describe Magento templates and layout files location

一个主题通常包含如下元素

layout文件夹中包含用于定义主题布局的XML文件,这些文件就如同连接module(app/code目录下)和模板文件的胶水一样。

由于Magento的模块特性,默认主题的所有XML文件都存储在以模块名命名的单独文件夹中。而非默认主题,则会存放在所有布局更新所使用的local.xml文件中。

template文件夹中存放显示在前台的各Magento block用到的.phtml文件,该文件中包含php和html代码

locale文件夹中按语言存放.csv文件,命名规则为languagecode_COUNTRYCODE/translate.csv,如en_US或en_UK/translate.csv

Describe Magento skin and JavaScript files location

skin文件夹中包含模板文件夹下.phtml文件所使用到的Javascript, CSS和图片文件,也就是说这个文件夹下的内容每个主题都是不同的。从下图中可以看到其中包含三个文件夹,CSS文件夹中存放主题用到的层叠样式表,images文件中存放主题使用的媒体文件,JS文件夹中存放模板用到的Javascript文件。

Magento下skin文件夹内容结构

js文件夹存放在前台和后台中使用的js文件,库文件和框架文件。如果要添加一个新的Javascript/AJAX库或者local代码池中要用到一些特殊的脚本,那么就都放到这里。

Magento根目录js文件夹下文档结构

Identify and explain the main Magento design areas (adminhtml and frontend)

Magento模板系统都由三个组件构成,有存储在叫做Block的模块文件夹中的php类,它们从数据库中加载数据并传输到主题PHP/HTML模板文件中(.phtml)。也有构建页面结构和组成部分的XML配置文件。

Magento的design目录结构

所有的前台文件都存放在图示design目录下的三个的文件夹中。

install 此文件夹存放安装过程中需使用到的内容

adminhtml 登录admin后台所看到的所有内容都放在这里。也就说如果模块需要用到任何与admin面板有关的内容,都可以在这里找到对应的模板和布局文件。

frontend 所有访客在前台看到内容都存放在这里。

 

那么完整的主题结构是什么样的呢?

Package

安装完Magento社区版后,会有两个包:base和default(后续版本中还有rwd),企业版中的名称为base和enterprise。

Themes

主题由一系列的layout, template, locale和skin文件组成,也就是网站的对外展示部分。Magento中可以加载多个主题,而主题可以分类两大类:

  • Default theme

每个包都会有一个叫做default的主题,Magento会自动搜索default主题并载入到前台中使用。要想要改变前台,可以个性化default主题或开发非默认主题,显然后者更为可取。

  • Non-default theme

非默认主题在default基础上进行修改或者通过CSS, 布局文件等形成完全不同的展现。

Explain class naming conventions and their relationship with the autoloader

Magento是在Zend框架的基础上进行开发的,所以其类的命名规则也取自Zend Framework。Magento中根据文件位置来对类名进行标准化,这样有助于类的自动加载(autoloader),而无需逐一require_once和include_once来包含文件。由于/在类名中属于非法字符,会使用下划线_来进行替代。

比如说Mage_Catalog_Model_Product类就在app/code/core/Mage/Catalog/Model/Product.php文件当中,Magento的autoloader将路径中的斜杠/全部替换成了下划线_,并在下面的文件夹中进行查找

app/code/core

app/code/community

app/code/local

lib/

注:app/code/local可通过将app/etc/local.xml文件中的如下字段设置为true来取消使用:

<disable_local_modules>false</disable_local_modules>

关于分类的搜索顺序,前面有提到是通过app/Mage.php文件中的如下代码来实现的

/**
     * Set include path
     */
    $paths[] = BP . DS . 'app' . DS . 'code' . DS . 'local';
    $paths[] = BP . DS . 'app' . DS . 'code' . DS . 'community';
    $paths[] = BP . DS . 'app' . DS . 'code' . DS . 'core';
    $paths[] = BP . DS . 'lib';
 
    $appPath = implode(PS, $paths);
    set_include_path($appPath . PS . Mage::registry('original_include_path'));
    include_once "Mage/Core/functions.php";
    include_once "Varien/Autoload.php";

Varien_Autoload类用于自动加载(lib/Varien/Autoload.php):

class Varien_Autoload
{
...
/**
     * Register SPL autoload function
     */
static public function register()
{
        spl_autoload_register(array(self::instance(), 'autoload'));
}
  /**
     * Load class source code
     *
     * @param string $class
     */
    public function autoload($class)
    {
...
   $classFile = str_replace(' ', DIRECTORY_SEPARATOR, ucwords(str_replace('_', ' ', $class)));
...
   $classFile.= '.php';
     return include $classFile;
    }

在Magento中创建对象

Magento提供了特别的方法用于创建model, helper和block,使用缩短了类名的Mage全局类。例如,获取Mage_Catalog_Model_Product模型,常会使用Mage :: getModel (‘catalog / product’)。

catalog – Mage_Catalog_Model的缩写
product – 用于决定用到的类

类名与缩写的名称的关联发生在模块的配置文件中(…/etc/config.xml)

<!--?xml version="1.0"?-->
 
                <!-- a shortened name -->
                Mage_Catalog_Model
 
                <!-- a shortened name -->
                Mage_Catalog_Block
 
                <!-- a shortened name -->
                Mage_Catalog_Helper

Describe methods for resolving module conflicts

首先需要确定是什么类型的错误,然后才能找到相对应的方案。一般有三种模块兼容性错误。

1) Conflicts in configuration files
2) Conflicts with the software part
3) Conflicts in a module display

Conflicts in configuration files

我们来讨论一下配置文件错误产生的原因以及解决方法。第一步查看位于app/etc/modules文件夹下的模块定义文件。在该文件中会设置模块是否active以及所属的代码池,另外还可以通过<depends></depends>标签对来设置依赖关系。

比如有两个模块使用同样的类依赖关系的话,可能会由于类构造器的不当使用而产生冲突。这类错误与后面会讲到的软件部分错误有着紧密的关联。如果遇到因<depends>的不当使用产生错误,可以通过设置其中一个依赖于另一个类,并在稍后的类继承中使用该依赖关系。方法为:

a) 将如下代码:

Namespace_OtherModulename.xml

<config>
    <modules>
        <Namespace_OtherModulename>
            <active>true</active>
            <codePool>community</codePool>
    <depends><Mage_Something/></depends>
        </Namespace_OtherModulename>
    </modules>
</config>

Namespace_Modulename.xml

<config>
    <modules>
        <Namespace_Modulename>
            <active>true</active>
            <codePool>community</codePool>
    <depends><Mage_Something/></depends>
        </Namespace_Modulename>
    </modules>
</config>

b)替换为如下代码:
Namespace_OtherModulename.xml

<config>
    <modules>
        <Namespace_OtherModulename>
            <active>true</active>
            <codePool>community</codePool>
    <depends><Mage_Something/></depends>
        </Namespace_OtherModulename>
    </modules>
</config>

Namespace_Modulename.xml

<config>
    <modules>
        <Namespace_Modulename>
            <active>true</active>
            <codePool>community</codePool>
    <depends><Namespace_OtherModulename/></depends>
        </Namespace_Modulename>
    </modules>
</config>

Conflicts with the software part

前面提到过产生模块错误的主要原因能过是依赖关系和类rewrite造成的。这应该查看模块配置文件 (/Namespace_Modulename/etc/ config.xml), 这里可以通过<rewrite></rewrite>标签对来进行类重写,比如:

<customer>
    <rewrite>
         <form_edit>Namespace_Modulename_Block_Rewrite_BlockClass</form_edit>
     </rewrite>
</customer>

Namespace_Modulename/etc/config.xml

<customer>
    <rewrite>
         <form_edit>Namespace_Modulename_Block_Rewrite_BlockClass</form_edit>
     </rewrite>
</customer>

Namespace_OtherModulename/etc/config.xml

<customer>
    <rewrite>
         <form_edit>Namespace_OtherModulename_Block_Rewrite_BlockClass</form_edit>
     </rewrite>
</customer>

1) 第一步删除第一个模块中的重写,代码如下:

<customer>
    <rewrite>
         <form_edit>Namespace_Modulename_Block_Rewrite_BlockClass</form_edit>
     </rewrite>
</customer>

2) 使用第一个类继承第二个类:

Class Namespace_Modulename_Block_Rewrite_BlockClass extends Namespace_OtherModulename_Block_Rewrite_BlockClass

尽管这一方法非常灵活,但却有着一些缺点,比如需要仔细查看这两个类中的相同函数,可能会存在返回值和parent::functionName();方法使用的问题。但这仍是在代码层面解决模块错误的重中之重。

Conflicts in a module display

在使用多个模块时最常见的错误是在前端显示模块的错误,发生这一错误有几个原因:在布局设置中的block重载,对同一block应用不同的模板,删除另一个模块中要内嵌的block。那么让我们来进一步看看会发生模块错误的地方:

主题配置文件:app/design/frontend/default/your_theme/layout

模板文件:app/design/frontend/default/your_theme/template

如果模块在前端不展示或者显示错误,极有可能错误就发生在这些地方。

首先查看你的文件和其它模块用于前台显示设置的模块文件,位于app/design/frontend/default/your_theme/layout。如果多个模块使用相同的block却应用了不同的模板,就需要修改为只使用一个模板。可在这个模板文件中并入其它去除掉的模板的代码。

另一个常见错误是把模块标准block的名称给修改了,这时其它通过<reference></reference>标签对使用这一block的模块就会出错。还有一些情况,模块使用了自己的模板却与其它模块不太兼容,这时就要具体情况具体对待了。

How does the framework interact with the various codepools?

我们在前面已经不止一次的提到,Magento会先包含local代码池,其次community,最后才是core代码池。通过这一顺序可以让开发人员在不直接修改core中的文件的情况下实现系统类的重载。

Core代码池

这个文件夹中存放着所有实现Magento强大、灵活的功能的核心代码,强烈建议不要修改其中的代码。

Community代码池

该文件来为第三方开发者所提供,所有第三方开发的插件代码默认都会存放在app/code/community文件夹中。

Local代码池

如果您想要自己对网站进行调整、想要修改某些逻辑或者重载系统中的类,都可以在local代码池中实现。

What constitutes a namespace and a module?

命名空间或者说工厂名请首字母大写这样有助于Autoloader的加载

What does the structure of a complete theme look like?

主题文件夹结构:

Magento主题文件夹结构

skin文件夹结构

Magento skin文件夹结构

Explain how Magento loads and manipulates configuration information

 

Magento Facebook, Twitter登录插件

社交网站已成功占据网民们越来越多的时间,不论是一个内容站还是电商站,社交分享按钮都早已成为标配。社交分享按钮在很多模板中都已添加,甚至在Magento自带的rwd包默认模板中也已加入了Facebook和Twitter的分享按钮,此外还可以采用AddThis这样的免费代码来集成分享功能。

Magento rwd社交分享按钮

分享以外为了降低获取用户的成本、改善用户体验各大网站也开始纷纷添加了Facebook等账号登录的功能,这样做的目的是一个避免了客户看到冗长的注册表格后产生的较高的跳出率,更深层的目的也是为了能够打入客户的关系链。

关于Magento社交登录有不少付费插件,这里不再赘述。今天介绍的这款免费插件是由做Magento开发比较资深的Inchoo网站发布的,最近一次的更新时间是2014年9月5号:

Inchoo社交登录插件

下载地址: http://pan.baidu.com/s/1e7A3c 密码: ttk2

更新地址:https://github.com/Marko-M/Inchoo_SocialConnect

下载后进行压缩将app和skin两个文件夹中的内容复制到Magento的安装根目录下,登录后台System > Configuration > Customers > Customer Configuration下会出现Social Connect Facebook Options等配置组

社交登录插件后台显示

以Facebook登录为例,若要开启此功能,打开Facebook开发者页面https://developers.facebook.com/,点击导航栏My Apps下的Add a New App,在弹出窗口中选择Website

Facebook中添加网站应用

在新出现的窗口中输入一个标识名称如localtest,点击下面的Create New Facebook App ID按钮

创建Facebook ID

然后在弹出窗口中选择一个分类如Business点击Create App ID

创建新APP ID

在新的页面中输入网址,这里在本地测试使用http://localhost/magento,点击Next

添加网址

此时再点击上面导航中的My Apps就会出现我们所创建的App(这里的名称为localtest),点击进入,就可以获取得App ID和App Secret(点击右边的Show按钮并输入密码验证),将这个两个值分别填入后台中的Facebook App ID和Facebook App Secret然后保存。

获取APP ID和密钥

接下来需要激活这个App,在同一个页面左侧导航中点击Settings然后在Contact Email下输入一个有交的email地址,保存然后点击左侧导航上的Status & Review按钮,此时将Do you want to make this app and all its live features available to the general public?右侧的滑块置为YES便可以在Magento中使用Facebook登录了

激活Facebook APP

 

此时访问http://localhost/magento/index.php/customer/account/login/等页面就会出现Facebook的登录按钮

Facebook登录

常见错误

1.在进行Facebook登录时出现App Not Setup: This app is still in development mode, and you don’t have access to it. Switch to a registered test user or ask an app admin for permissions.报错

这是因为没有激活所创建的App,请参照本节激活步骤

Facebook登录报错

2.集成后出现Unable to Connect to ssl://graph.facebook.com:443. Error #154298408: Unable to find the socket transport “ssl” – did you forget to enable it when you configured PHP?报错

这很明显是由于未开启ssl,以XAMPP为例,打开php.ini文件,并取消如下语句前的分号注释,保存并重启Apache

extension=php_openssl.dll

Linux中为extension=php_openssl.so

 

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中各处用于生成配置过图片的完整路径,应该只是作用于后台配置中的图片预览。