Magento开发文档(五):Magento模型与ORM基础
模型层的实现是任何一个MVC框架的重要组成部分。它用来实现应用程序的数据,并且大部分应用程序在没有数据的情况下都是一堆废柴。相对于其他PHP MVC框架,Magento模型在系统中扮演了一个更为重要的角色,因为它包含了通常应用于控制器和助手方法中的业务逻辑。
传统的PHP MVC模型
如果说MVC架构的定义有些模糊,那么模型的定义就更为模糊了。早在MVC模式被PHP开发者普遍接受之前,数据的交互通常是使用原始的SQL语句或者SQL抽象进行。开发者必须很多数据库查询语句,而不用考虑在模型化哪个对象。
此处省略三段关于传统PHP MVC模型层以及ORM的探讨,直接进入正题。
Magento 模型
毫无疑问Magento实现了ORM模式。尽管Zend Framework的SQL抽象层能够正常使用,大部分的数据交互依然是通过内置的Magento模型,以及用户自己构建的模型完成。Magento系统拥有一个高度灵活,高度抽象的模型层。
Magento模型解剖
绝大部分Magento模型可以被分为两类。基础的,ActiveRecord,或者说是“一张表,一个对象”的模型;另外一种是Entity Attribute Value(EAV)模型。每个模型都包含一个模型收集(Model Collection)。收集(Collections)是用来同时操作多个Magento模型实例的对象。Magento团队通过实现PHP的IteratorAggregate标准库接口和Countable,从而允许每个模型类型拥有自己的收集类型。如果你对PHP标准库不是很熟悉,可以将模型收集想象成拥有方法可以使用的数组。
Magento模型不包含任何连接数据库的代码。取而代之,每个模型使用两个modelResource类(一个读取,一个写入),它们通过read and write adapter objects与数据库进行交互。通过解耦模型与数据库交互代码,理论上可以通过构建新的资源类来满足任意不同的数据库平台,并且保持模型的完整性。
创建一个基础的Magento模型
下面我们开始创建一个基础的Magento模型,我们以简单的weblog博客为例,构建一个模型,总的分为以下几步。
- 创建“Weblog”模块
- 为模型创建一张表,模型命名为Blogpost
- 添加模型信息到配置文件
- 添加模型资源信息到配置文件
- 添加Read Adapter信息到配置文件
- 添加Write Adapter信息到配置文件
- 为Blogpost模型添加PHP类文件
- foo
- 初始化模型
创建 Weblog 模块
通过之前几章的学习,创建一个新的空模块应该没有问题啦,这里我们跳过这些细节,假设你已经创建了一个名为Weblog的空模块。完成之后,我们为Index控制器设置路由规则。这里依然假设我们的Package命名为Magentotutorial。
在Magentotutorial/Weblog/etc/config.xml文件中,加入一下路由规则,
01 02 03 04 05 06 07 08 09 10 11 |
< frontend > < routers > < weblog > < use >standard</ use > < args > < module >Magentotutorial_Weblog</ module > < frontName >weblog</ frontName > </ args > </ weblog > </ routers > </ frontend > |
然后添加以下代码到Index控制器中,该文件位于Magentotutorial/Weblog/controller/IndexController.php。
01 02 03 04 05 |
class Magentotutorial_Weblog_IndexController extends Mage_Core_Controller_Front_Action { public function testModelAction() { echo 'Setup!' ; } } |
清空Magento缓存,根据你的安装路径,访问类似下面的地址,
http://example.com/weblog/index/testModel
创建数据库表
Magento系统能够自动创建和更改数据库模式,这里为了演示,我们先手动为模型创建一个表。使用命令行或你最喜欢的MySQL GUI工具,创建下表,
01 02 03 04 05 06 07 08 |
CREATE TABLE `blog_posts` ( `blogpost_id` int (11) NOT NULL auto_increment, `title` text, `post` text, ` date ` datetime default NULL , ` timestamp ` timestamp NOT NULL default CURRENT_TIMESTAMP , PRIMARY KEY (`blogpost_id`) ) |
然后填充一些数据到表中,
01 |
INSERT INTO `blog_posts` VALUES (1, 'My New Title' , 'This is a blog post' , '2010-07-01 00:00:00' , '2010-07-02 23:12:30' ); |
创建模型及其配置文件
创建Weblog的模型及其配置文件需要以下五步完成,
- 在模块中启用模型
- 在模块中启用模型资源(Model Resources)
- 在模型资源中添加实体“entity”,对于简单的模型来说,该实体即表名
- 为模型资源指定读取适配器(Read Adapter)
- 为模型资源指定写入适配器(Writer adapter)
在Magento中实例化一个模型,可以使用如下语法,
01 |
$model = Mage::getModel( 'weblog/blogpost' ); |
getmodel()方法里的URI的第一部分叫做模型组名(Model Group Name)。考虑到Magento为类使用__autoload方法,所以该模型组名必须是模块的小写形式。该URI的第二部分是你的模型名的小写形式。
接着,我们开始添加模型的配置代码到模块的config.xml文件中。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 |
< global > <!-- ... --> < models > < weblog > < class >Magentotutorial_Weblog_Model</ class > <!-- need to create our own resource, cant just use core_mysql4 --> < resourceModel >weblog_mysql4</ resourceModel > </ weblog > </ models > <!-- ... --> </ global > |
最外层的<weblog />标签是模型组名,应该匹配模块名。<class />中的值是weblog组中所有的模型都拥有的BASE名。<resourceModel />标签指定weblog组中的模型应该使用哪种模型资源,这里我们先记得它是由模型组名加“mysql4”。
现在让我们清理下Magento缓存,尝试下实例化这个blogpost模型。在testModelAction()中,添加如下代码。
01 02 03 04 |
public function testModelAction() { $blogpost = Mage::getModel( 'weblog/blogpost' ); echo get_class( $blogpost ); } |
刷新页面之后,你会看到系统抛出了异常,大概如下,
include(Magentotutorial/Weblog/Model/Blogpost.php) [function.include]: failed to open stream: No such file or directory
由于在上面那段代码中,试图引用‘weblog/blogpost’模型,Magento会实例化下面这个类,
Magentotutorial_Weblog_Model_Blogpost
但是此时我们还没有创建这个文件。所以系统会抛出上面的异常。下面我们来创建该类,文件路径位于,
File: app/code/local/Magentotutorial/Weblog/Model/Blogpost.php
01 02 03 04 05 06 07 |
class Magentotutorial_Weblog_Model_Blogpost extends Mage_Core_Model_Abstract { protected function _construct() { $this ->_init( 'weblog/blogpost' ); } } |
刷新页面之后,异常就被该类名所取代了。所有的基础模型都必须扩展Mage_Core_Model_Abstract类。这个抽象类强制你必须实现一个名为_construct的方法。此方法会调用该类的_init方法,并需要传递在getModel()方法中的参数。
全局配置和模型资源
到此为止,我们已经成功设置了自定义的模型。接着,我们需要设置它的模型资源。模型资源包含与数据库交互的代码。在上一小节中,我们在配置文件中添加了如下代码,
01 |
< resourceModel >weblog_mysql4</ resourceModel > |
在<resourceModel />中的值会实例化一个模型资源类。尽管你从不需要手动调用它,当任何在weblog组中的模型需要与数据库交互时,Magento会调用以下方法获取模型资源,
01 |
Mage::getResourceModel( 'weblog/blogpost' ); |
重申一次,weblog是模型组名,blogpost是模型名。Mage::getResourceModel方法使用weblog/blogpost URI来检查全局配置文件,并获取<resourceModel>中的值(在这里,是weblog_mysql4)。然后,下列URI地址的模型类将会被实例化。
weblog_mysql4/blogpost
资源模型的配置与模型的配置在XML配置文件中的相同节点呢,下面我们在<models>节点中添加下列代码,
01 02 03 04 05 06 07 08 09 |
< global > <!-- ... --> < models > <!-- ... --> < weblog_mysql4 > < class >Magentotutorial_Weblog_Model_Mysql4</ class > </ weblog_mysql4 > </ models > </ global > |
这里设置的<weblog_mysql4 />标签,就是刚刚在<resourceModel />标签中设置的值。<class />节点中的值是使用的资源模型的基础命名,它的命名方式大概如下
Packagename_Modulename_Model_Mysql4
现在,我们成功配置了资源模型,来试着从模型数据中读取一些信息吧。稍稍添加一些代码到testModelAction()方法中。
01 02 03 04 05 06 07 08 |
public function testModelAction() { $params = $this ->getRequest()->getParams(); $blogpost = Mage::getModel( 'weblog/blogpost' ); echo ( "Loading the blogpost with an ID of " . $params [ 'id' ]); $blogpost ->load( $params [ 'id' ]); $data = $blogpost ->getData(); var_dump( $data ); } |
清空Magento缓存,在浏览器中打开如下地址,
http://example.com/weblog/index/testModel/id/1
好吧,又一次看到系统抛出了异常,大概如下,
Warning: include(Magentotutorial/Weblog/Model/Mysql4/Blogpost.php) [function.include]: failed to open stream: No such file ….
上面我们提到过,当与数据库交互时,会实例化资源模型类,这里系统提示我们需要为该模型添加一个模型资源类。(译者注:本文提到过,Magento的模型本身与数据库连接及交互是相互独立的,所以在模型没有与数据库交互之前,例如在本篇第一次使用getModel()方法时,系统不会抛出关于实例化模型以外的异常。)每个模型都有模型资源类,添加该类到下列路径的文件中,
File: app/code/local/Magentotutorial/Weblog/Model/Mysql4/Blogpost.php
01 02 03 04 05 06 |
class Magentotutorial_Weblog_Model_Mysql4_Blogpost extends Mage_Core_Model_Mysql4_Abstract{ protected function _construct() { $this ->_init( 'weblog/blogpost' , 'blogpost_id' ); } } |
可以看到,_init方法的第一个参数依旧是模型组名/模型名。参数二是数据库字段,可以是任意唯一字段,大多数情况下,参数二可以指定为主键。清空缓存,刷新页面,页面中会显示如下内容,
Loading the blogpost with an ID of 1
array
empty
没有异常?可是也没有正常读取到数据!接着该做些什么呢?每一个模型组都拥有一个读取适配器和写入适配器。Magento允许模型使用默认的适配器,也可以使用开发者自己开发的适配器。无论使用哪一种,我们需要告诉Magento系统关于适配器的配置。这里,我们在配置文件中添加一个新的tag节点,<resources />到<global />节点中。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 |
< global > <!-- ... --> < resources > < weblog_write > < connection > < use >core_write</ use > </ connection > </ weblog_write > < weblog_read > < connection > < use >core_read</ use > </ connection > </ weblog_read > </ resources > </ global > |
这里我们在<resources />中添加了两个子节点。一个用来写入,另一个用来读取。标签命名(<weblog_write />和<weblog_read />)根据上面定义的模型组名。完成改配置文件之后,清空Magento缓存,再次刷新页面,然后…
Can’t retrieve entity config: weblog/blogpost
又一次出现异常了!一起理清下思路,在使用模型URI weblog/blogpost时,Magento系统被告知我们想使用模型组weblog,以及blogpost实体。在扩展Mage_Core_Model_Mysql4_Abstract的简单模型中,实体相对应一张表。这里,该表即我们上面创建的blog_post表,添加该实体到配置文件中。
01 02 03 04 05 06 07 08 09 10 11 |
< models > <!-- ... ---> < weblog_mysql4 > < class >Magentotutorial_Weblog_Model_Mysql4</ class > < entities > < blogpost > < table >blog_posts</ table > </ blogpost > </ entities > </ weblog_mysql4 > </ models > |
在配置文件中的resource模型节点中,添加新的<entities />节点。现在,在配置文件中终于出现了以刚才创建的表名命名的节点,从而为该模型指定相关的数据库表。
清空Magento缓存,刷新页面,OK…
Loading the blogpost with an ID of 2
Loading the blogpost with an ID of 1
array
‘blogpost_id’ => string ’1′ (length=1)
‘title’ => string ‘My New Title’ (length=12)
‘post’ => string ‘This is a blog post’ (length=19)
‘date’ => string ’2009-07-01 00:00:00′ (length=19)
‘timestamp’ => string ’2009-07-02 16:12:30′ (length=19)
好啦!经过这么长一个过程我们终于成功从数据库中读取到了数据,更为重要的是,我们完成了一个崭新的Magento模型的配置!
基础的Magento模型操作
Magento模型都继承自Varien_Object类。该类是Magento系统核心库中的一部分,而非Magento核心模块。可以在下列路径找到该对象。
lib/Varien/Object.php
Magento模型将数据保存在一个protected的_data属性中。Varien_Object类提供给我们很多方法,可以使用这些方法读取这些数据。你已经使用过了getData()方法,该方法返回一个包含字段/值的数组。你也可以通过传递字段名作为该方法的参数来获取相应字段的值
01 02 |
$model ->getData(); $model ->getData( 'title' ); |
还有一个getOrigData方法,which will return the Model data as it was when the object was initially populated, (working with the protected _origData method).这段就不翻译了。
01 02 |
$model ->getOrigData(); $model ->getOrigData( 'title' ); |
Varien_Object类通过PHP的魔术方法__call实现了一些特殊的方法。你可以通过get,set,unset以及has加上驼峰命名的字段名的方式,获取、设置、unset及查看任意存在的字段值。
01 02 03 04 |
$model ->getBlogpostId(); $model ->setBlogpostId(25); $model ->unsetBlogpostId(); if ( $model ->hasBlogpostId()){...} |
正因为如此,你可能会以小写字母及下划线来命名数据库字段。不过,最近版本的Magento已经舍弃了这种语法,转而实现PHP的数组连接(ArrayAccess)接口。
01 02 03 |
$id = $model ->[ 'blogpost_id' ]; $model ->[ 'blogpost_id' ] = 25; //etc... |
That said, you’re likely to see both techniques used throughout the Magento code base, as well as third party extensions.这段意思应该是说你可以在Magento或第三方扩展中看到上面两种语法格式。
Magento的CRUD操作
Magento模型通过load(),sava(),delete()方法,提供基础的Create,Read,Update和Delete功能。在上面的控制器方法中,我们已经使用了load()方法。当传递一个参数到load()方法中,该方法会返回与该参数相对应的id字段(在模型资源中设置)的一条记录。
01 |
$blogpost ->load(1); |
save()方法允许你插入新数据到模型中,或更新已经存在的数据。添加如下代码到控制器中。
01 02 03 04 05 06 07 |
public function createNewPostAction() { $blogpost = Mage::getModel( 'weblog/blogpost' ); $blogpost ->setTitle( 'Code Post!' ); $blogpost ->setPost( 'This post was created from code!' ); $blogpost ->save(); echo 'post created' ; } |
然后在浏览器中访问以下地址,
http://example.com/weblog/index/createNewPost
这时你会看到数据库表中新增了一条数据,然后在控制器中加入编辑功能。
01 02 03 04 05 06 07 |
public function editFirstPostAction() { $blogpost = Mage::getModel( 'weblog/blogpost' ); $blogpost ->load(1); $blogpost ->setTitle( "The First post!" ); $blogpost ->save(); echo 'post edited' ; } |
最后,加入下列代码,实现删除功能。
01 02 03 04 05 06 |
public function deleteFirstPostAction() { $blogpost = Mage::getModel( 'weblog/blogpost' ); $blogpost ->load(1); $blogpost -> delete (); echo 'post removed' ; } |
Magento的模型收集 Model Collections
对于单独一个模型的操作固然很有用,但是多数时候,我们会同时操作多个模型。比返回多个模型的一个多维嵌套数组更好的是,在Magento中,每个模型类型都有一个唯一的收集对象与其关联。这些对象实现了PHP IteratorAggregate和Countable接口,这意味着它们可以被传递到count函数,并使用for each结构循环出数据。
我们将在第八章具体介绍Magento的收集机制,现在我们先简要介绍下它的设置和使用。添加如下代码到控制器中,然后再浏览器中访问该地址。
01 02 03 04 05 06 07 |
public function showAllBlogPostsAction() { $posts = Mage::getModel( 'weblog/blogpost' )->getCollection(); foreach ( $posts as $blog_post ){ echo '<h3>' . $blog_post ->getTitle(). '</h3>' ; echo nl2br ( $blog_post ->getPost()); } } |
访问如下地址,
http://example.com/weblog/index/showAllBlogPosts
然后,是的,系统再一次抛出异常。
Warning: include(Magentotutorial/Weblog/Model/Mysql4/Blogpost/Collection.php) [function.include]: failed to open stream
看下上面的PHP代码,你就应该对系统抛出异常不会感到太惊讶了吧?我们需要添加一个类来定义Blogpost的模型收集。每个模型资源拥有一个_resourceCollectionName保护属性,它包含了用来识别收集的URI。
01 |
protected '_resourceCollectionName' => string 'weblog/blogpost_collection' |
默认的,该URI也用来识别模型资源,以字符串”_collection”结尾。Magento将收集归为模型资源的一部分,所以该URI转换为类名之后如下,
Magentotutorial_Weblog_Model_Mysql4_Blogpost_Collection
添加下面的模型收集类到如下路径,
File: app/code/local/Magentotutorial/Weblog/Model/Mysql4/Blogpost/Collection.php
01 02 03 04 05 06 |
class Magentotutorial_Weblog_Model_Mysql4_Blogpost_Collection extends Mage_Core_Model_Mysql4_Collection_Abstract { protected function _construct() { $this ->_init( 'weblog/blogpost' ); } } |
和其他类一样,我们需要使用该模型的URI(weblog/blogpsot)来_init模型收集。最后,在浏览器中访问模型收集的地址,就能成功返回文章的数据信息了。
Magento核心模型
恭喜你,到这里,说明你已经完成了Magento模型的配置。在后面的教程中,我们会更深入的讲解Magento的高级EAV模型。
还要提到一点,上面文章中,我们说到所有的Magento模型都继承自Mage_Core_Model_Abstract类。这并不是100%正确。因为有些模型直接继承自Varien_Object。当然,这都不会影响到任何你创建的模型,说明这些只是为了让开发者能够更好的理解Magento的代码。