清理我的应用程序结构...域对象和数据映射器

时间:2014-06-17 17:46:08

标签: php zend-framework activerecord datamapper domain-object

我已经开发了一个使用ZendFramework 1.1的应用程序,这个应用程序在两年的大部分时间里已经开始了,并且因此我已经看到了一些从我学习或尝试新事物的重构的不同阶段。在目前的状态下,我觉得我的结构非常好因为我可以快速完成任务,但肯定可以在某些方面使用一些改进 - 我觉得有很多膨胀和尴尬的依赖。

在我的应用程序中放置一些示例代码时,请耐心等待。我将使用Order对象的示例,该对象具有OrderItem个实例,这些实例也必须保存。我将解释实例化和保存的所有必要部分。

就我的理解而言,我在这里所发生的事情更符合ActiveRecord设计模式而不是Domain Models,尽管我认为我有两种做法。 ..

class Order extends BaseObject {
    /** @var OrderItem array of items on the order */
    public $items = array();

    public function __construct($data = array()){

        // Define the attributes for this model
        $schema = array(
            "id" => "int", // primary key
            "order_number" => "string", // user defined
            "order_total" => "float", // computed
            // etc...
        );

        // Get datamapper and validator classes
        $mf = MapperFactory::getInstance();
        $mapper = $mf->get("Order");
        $validator = new Order_Validator();
        $table = new Application_DbTable_Order();

        // Construct parent
        parent::__construct($schema, $mapper, $validator, $table);

        // If data was provided then parse it
        if(count($data)){
            $this->parseData($data);
        }

        // return the instance
        return $this;
    }

    // Runs before a new instance is saved, does some checks
    public function addPrehook(){
        $orderNumber = $this->getOrderNumber();
        if($this->mapper->lookupByOrderNumber($orderNumber)){
            // This order number already exists!
            $this->addError("An order with the number $orderNumber already exists!");
            return false;
        }

        // all good!
        return true;
    }

    // Runs after the primary data is saved, saves any other associated objects e.g., items
    public function addPosthook(){
        // save order items
        if($this->commitItems() === false){
            return false;
        }

        // all good!
        return true;
    }

    // saves items on the order
    private function commitItems($editing = false){
        if($editing === true){
            // delete any items that have been removed from the order
            $existingOrder = Order::getById($this->getId());
            $this->deleteRemovedItems($existingOrder);
        }

        // Iterate over items
        foreach($this->items as $idx => $orderItem){
           // Ensure the item's order_id is set!
           $orderItem->setOrderId($this->getId());

           // save the order item
           $saved = $orderItem->save();
           if($saved === false){
               // add errors from the order item to this instance
               $this->addError($orderItem->getErrors());

               // return false
               return false;
           }

           // update the order item on this instance
           $this->items[$idx] = $saved;
        }

        // done saving items!
        return true;
    }

    /** @return Order|boolean The order matching provided ID or FALSE if not found */
    public static function getById($id){
        // Get the Order Datamapper
        $mf = MapperFactory::getInstance();
        $mapper = $mf->get("Order");

        // Look for the primary key in the order table
        if($mapper->lookup($id)){
            return new self($mapper->fetchObjectData($id)->toArray());
        }else{
            // no order exists with this id
            return false;
        }
    }
}

解析数据,保存以及几乎所有适用于所有模型的东西(一个更合适的术语可能是实体)都存在于BaseObject中,如下所示:

class BaseObject {
    /** @var array Array of parsed data */
    public $data;

    public $schema; // valid properties names and types
    public $mapper; // datamapper instance
    public $validator; // validator instance
    public $table; // table gateway instance

    public function __construct($schema, $mapper, $validator, $table){
        // raise an error if any of the properties of this method are missing

        $this->schema = $schema;
        $this->mapper = $mapper;
        $this->validator = $validator;
        $this->table = $table;
    }

    // parses and validates $data to the instance
    public function parseData($data){
        foreach($data as $key => $value){

            // If this property isn't in schema then skip it
            if(!array_key_exists($key, $this->schema)){
                continue;
            }

            // Get the data type of this
            switch($this->schema[$key]){
                case "int": $setValue = (int)$value; break;
                case "string": $setValue = (string)$value; break;
                // etc...
                default: throw new InvalidException("Invalid data type provided ...");
            }

            // Does our validator have a handler for this property?
            if($this->validator->hasProperty($key) && !$this->validator->isValid($key, $setValue)){
                $this->addError($this->validator->getErrors());
                return false;
            }

             // Finally, set property on model
            $this->data[$key] = $setValue;    
        }
    }

    /**
     * Save the instance - Inserts or Updates based on presence of ID
     * @return BaseObject|boolean The saved object or FALSE if save fails
     */
    public function save(){
        // Are we editing an existing instance, or adding a new one?
        $action   = ($this->getId()) ? "edit" : "add";
        $prehook  = $action . "Prehook";
        $posthook = $action . "Posthook";

        // Execute prehook if its there
        if(is_callable(array($this, $prehook), true) && $this->$prehook() === FALSE){
            // some failure occured and errors are already on the object
            return false;
        }

        // do the actual save
        try{ 
            // mapper returns a saved instance with ID if creating
            $saved = $this->mapper->save($this);
        }catch(Exception $e){
            // error occured saving
            $this->addError($e->getMessage());
            return false;
        }

        // run the posthook if necessary
        if(is_callable(array($this, $posthook), true) && $this->$posthook() === FALSE){
            // some failure occured and errors are already on the object
            return false;
        }

        // Save complete!
        return $saved;
    }
}

基础DataMapper类包含saveinsertupdate的非常简单的实现,因为{{从不重载每个对象定义1}}。我觉得这有点不稳定,但我觉得它有效吗? $schema的子类基本上只提供特定于域的查找程序功能,例如BaseMapperlookupOrderByNumber以及其他类似的内容。

findUsersWithLastName

我觉得我拥有的并不一定是可怕的,但我也觉得这里有一些不太好的设计。我的担忧主要是:

模型有一个非常严格的结构,它与数据库表模式紧密耦合,使得从模型或数据库表中添加/删除属性是一个完全痛苦的屁股!我觉得将所有保存到数据库中的对象保存到构造函数中的class BaseMapper { public function save(BaseObject $obj){ if($obj->getId()){ return $this->update($obj); }else{ return $this->insert($obj); } } private function insert(BaseObject $obj){ // Get the table where the object should be saved $table = $obj->getTable(); // Get data to save $saveData = $obj->getData(); // Do the insert $table->insert($saveData); // Set the object's ID $obj->setId($table->getAdapter()->getLastInsertId()); // Return the object return $obj; } } $table是一个坏主意...... 如何避免这种情况?我该怎么做才能避免定义$mapper

验证似乎有点古怪,因为它与模型上的属性名称紧密相关,这些属性名称也对应于数据库中的列名。这使得任何数据库或模型更改变得更加复杂! 是否有更合适的验证位置?

DataMappers 除了提供一些复杂的查找程序功能外,并没有做太多的事情。保存复杂对象完全由对象类本身处理(例如,在我的示例中为$schema类。除了“复杂对象”之外,还有适合这种类型对象的术语吗?我说我的{{1对象是“复杂的”,因为它必须保存Order个对象。 DataMapper应该处理Order类中当前存在的保存逻辑吗?

非常感谢您的时间和投入!

1 个答案:

答案 0 :(得分:1)

尽可能多地分离对象之间的关注点是一种很好的做法。有一个负责输入验证,其他负责执行业务逻辑,数据库操作等。为了保持2个对象松散耦合,他们不应该只知道彼此的实现。这是通过接口定义的。

我建议您阅读这篇文章http://www.javaworld.com/article/2072302/core-java/more-on-getters-and-setters.html和其他人。他有一本书值得一读http://www.amazon.com/Holub-Patterns-Learning-Looking-Professionals/dp/159059388X

如果可能的订单和商品,我会分开,我不太了解您的应用程序,但如果您只需要显示包含订单号的20个订单的列表,那么这些数据库调用和处理订单项目将是浪费如果不分开。这当然不是唯一的方法。

首先,您需要知道订单属性是什么,并封装了将这些属性提供给订单的方法,并且还有一个订单将该数据公开给其他对象。

interface OrderImporter {

    public function getId();

    public function getOrderNumber();

    public function getTotal();
}

interface OrderExporter {

    public function setData($id, $number, $total);
}

为了将业务逻辑与数据库分开,我们需要封装该行为,如此

interface Mapper {

    public function insert();

    public function update();

    public function delete();
}

另外,我会定义一个特定的映射器,其职责是处理有关订单的DB操作。

interface OrderMapper extends Mapper {

    /**
     * Returns an object that captures data from an order
     * @return OrderExporter
     */
    public function getExporter();

    /**
     * @param string $id
     * @return OrderImporter
     */
    public function findById($id);
}

最后,订单需要能够通过一些消息与所有这些对象进行通信。

interface Order {

    public function __construct(OrderImporter $importer);

    public function export(OrderExporter $exporter);

    public function save(OrderMapper $orderRow);
}

到目前为止,我们有一种向订单提供数据的方法,一种从订单中提取数据的方法以及与数据库交互的方式。

下面我提供了一个非常简单的示例实现,远非完美。

class OrderController extends Zend_Controller_Action {

    public function addAction() {
        $requestData = $this->getRequest()->getParams();
        $orderForm = new OrderForm();

        if ($orderForm->isValid($requestData)) {
            $orderForm->populate($requestData);
            $order = new ConcreteOrder($orderForm);

            $mapper = new ZendOrderMapper(new Zend_Db_Table(array('name' => 'order')));
            $order->save($mapper);
        }
    }

    public function readAction() {
        //if we need to read an order by id
        $mapper = new ZendOrderMapper(new Zend_Db_Table(array('name' => 'order')));
        $order = new ConcreteOrder($mapper->findById($this->getRequest()->getParam('orderId')));
    }

}

/**
 * Order form can be used to perform validation and as a data provider
 */
class OrderForm extends Zend_Form implements OrderImporter {

    public function init() {
        //TODO setup order input validators
    }

    public function getId() {
        return $this->getElement('orderID')->getValue();
    }

    public function getOrderNumber() {
        return $this->getElement('orderNo')->getValue();
    }

    public function getTotal() {
        return $this->getElement('orderTotal')->getValue();
    }

}

/**
 * This mapper also serves as an importer and an exporter
 * but clients don't know that :)
 */
class ZendOrderMapper implements OrderMapper, OrderImporter, OrderExporter {

    /**
     * @var Zend_Db_Table_Abstract
     */
    private $table;
    private $data;

    public function __construct(Zend_Db_Table_Abstract $table) {
        $this->table = $table;
    }

    public function setData($id, $number, $total) {
        $this->data['idColumn'] = $id;
        $this->data['numberColumn'] = $number;
        $this->data['total'] = $total;
    }

    public function delete() {
        return $this->table->delete(array('id' => $this->data['id']));
    }

    public function insert() {
        return $this->table->insert($this->data);
    }

    public function update() {
        return $this->table->update($this->data, array('id' => $this->data['id']));
    }

    public function findById($id) {
        $this->data = $this->table->fetchRow(array('id' => $id));
        return $this;
    }

    public function getId() {
        return $this->data['idColumn'];
    }

    public function getOrderNumber() {
        return $this->data['numberColumn'];
    }

    public function getTotal() {
        return $this->data['total'];
    }

    public function getExporter() {
        return $this;
    }

}

class ConcreteOrder implements Order {

    private $id;
    private $number;
    private $total;

    public function __construct(OrderImporter $importer) {
        //initialize this object
        $this->id = $importer->getId();
        $this->number = $importer->getOrderNumber();
        $this->total = $importer->getTotal();
    }

    public function export(\OrderExporter $exporter) {
        $exporter->setData($this->id, $this->number, $this->total);
    }

    public function save(\OrderMapper $mapper) {
        $this->export($mapper->getExporter());

        if ($this->id === null) {
            $this->id = $mapper->insert();
        } else {
            $mapper->update();
        }
    }

}