Search code examples
zend-framework2parent-childzend-dbone-to-one

Zend\Db Model with Child Models


ZF2 project - no Doctrine, using native Zend\Db: Have the following structure:

Controller 
    ProductController
Model
    Product
    ProductTable
    ProductType
    ProductTypeTable

Product is the model, has variables corresponding to the “products" table fields.

ProductTable is table class which is connected to the database via tableGateway. ProductTable has getItem() method to retrieve requested product by “id”.

ProductType is the model, has variables like id, name, description corresponding to the “productTypes" table fields.

ProductTypeTable is table class just like ProductTable.

Each product belongs to a certain ProductType

products.productTypeId = productTypes.id 

is the relation.

In ProductTable->getItem() method, I can simply get productTypeId. I can use joins to get productTypes.name, productTypes.description, or any field from "productTypes" table. But I don’t want to do this - instead dealing with new variables in Product entity like productTypeName, productTypeDesc, I’d like to have Product->getProductType() and set it to be a ProductType object, so I can get Product->getProductType() ->getName() to get product type name.

Simply I’d like to assign a child model as a variable of the parent model.

I can do this in the controller like below:

$product = $this->getProductTable()->getItem(7); // id = 7 
$product->setProductType($this->getProductTypeTable()
                    ->getItem($product->getProductTypeId());

But I’d like to make it happen in product table class getItem() method. So I don’t have to think about it in every controller, and it is kind of encapsulated.

What is the right way to do this?

Thank you.


Solution

  • The issue that you have is the Table Gateway pattern is only really any good at abstracting database access to a a single database table. It does not in anyway allow for the hydration of entities or management of relationships. Object Relationship Mappers (ORM's), such as Doctrine, solve this problem.

    If Doctrine, for whatever reason, is inappropriate for your use case an alternative could be implementing the Data Mapper Pattern

    The Data Mapper is a layer of software that separates the in-memory objects from the database. Its responsibility is to transfer data between the two and also to isolate them from each other

    The data mapper will use the table gateway to fetch the required data for each table and construct the Product instance, including it's associated ProductType. You would then expose the mapper to the controller (rather than the table gateway).

    A simple example of a ProductMapper.

    class ProductMapper
    {
        // @var  \Zend\Db\TableGateway\TableGateway
        protected $productTable;
    
        protected $productTypeMapper;
    
        // an 'identity map' of loaded products
        protected $loaded = []; 
    
        public function __construct(ProductTable $productTable, ProductTypeMapper $productTypeMapper)
        {
            $this->productTable = $productTable;
            $this->productTypeMapper = $productTypeMapper;
        }
    
        protected function hydrate(Product $product, array $data)
        { 
            $product->setId($data['id']);
            $product->setName($data['name']);
            $product->setFoo($data['foo']);
    
            if (isset($data['type_id'])) {
                // Load a fully constructed product type from the database
                $type = $this->productTypeMapper->findById($data['type_id']);
                $product->setType($type);
            }
    
            return $product;
        }
    
        public function findById($id)
        {
            if (isset($this->loaded[$id])) {
                return $this->loaded[$id];
            }
            // Get the data
            $row = $this->productTable->select(['id' => $id]);
    
            if (empty($row)) {
                throw new SomeCustomException("No product could be found with id $id");
            }
            // Create and hydrate the product
            $product = $this->hydrate(new Product, $row->current())
            $this->loaded[$id] = $product;
    
            return $product;
        }
    
        public function save(array $data);
    
        public function update($data);
    
        public function delete($id);
    }