Learning Zend Framework after Magento: Models
Asked Answered
R

4

9

I have been working over an year with Magento and have learned it good enough. Now I want to learn Zend, and I'm stuck with models.

I'm used to have entities and collection of entities in Magento, and it's likely that I'll want to use Zend_Db_Table, Zend_Db_Table_Row and/or Zend_Db_Table_Rowset. What I am confused of is the role each class.

I know that I can extend each class, and I understand that in my Product_Table class (that extends Zend_Db_Table_Abstract) it's possible to have private methods that will tell Zend what classes to use for rows and rowsets, however I'm not feeling comfortable with it.

Having this code in Magento:

Example 1

// I understand that maybe I'll use the `new` keyword instead
// Mage::getModel() is only for exemplification
$product = Mage::getModel('catalog/product');
$product->setName('product name');
$product->setPrice(20);
$product->save();

if($id = $product->getId()){
    echo 'Product saved with id' . $id;
}
else{
    echo 'Error saving product';
}

Example 2

$collection = Mage::getModel('catalog/product')->getCollection();
// this is the limit, I'm ok with other method's name
$collection->setPageSize(10);
$collection->load()

foreach($collection as $product){
    echo $product->getName() . ' costs ' . $product->getPrice() . PHP_EOL;
}

How I can implement something similar in Zend Framework? Alternatively if this is a really a bad idea, what are the best practices to implement models in Zend Framework?

Thanks

Rile answered 19/8, 2011 at 16:30 Comment(5)
The problem with Zend Framework is that you can do models in several ways. There isn't any set way of constructing them. You just have to find what works for you and stick to it. Personally, I don't subclass any of the Zend_Db classes.Samovar
@SimpleCoder I agree. Zend_Db != model. This is most common newcomers mistake. Just because it somehow related to data in model they assume it IS model.Arvell
I understand that ZF doesn't have models. The question is how I can implement models?Rile
@SimpleCoder I wouldn't say that not having a standard Model implementation in ZF is a problem. It allows you to set up your data in a way that makes sense to you and your application. I will however agree with not extending Models from Zend_Db classes. Use Zend_Db_Select etc from within the models.Jamijamie
@adlawson: Agreed. For me at least, when I created my first ZF project, it just added to the learning curve. Now that I'm comfortable with ZF I'll never go back to not using a framework.Samovar
P
5

The Zend team, as mentioned elsewhere, thinks differently about the Model layer than most other PHP Framework creators. Their current thoughts on "the best" way to use their raw tools to provide a Database backed Entity Model can be found in the quick start guide.

That said, most people's solution to Models in Zend Framework is bootstrapping Doctrine.

Polymorphonuclear answered 22/8, 2011 at 4:12 Comment(3)
I'm not feeling OK with the solution from the quick start guide just because I should write a lot of code that I'd could just put in some magic methods. However, my first experiment with setters and getters failed and I'm neither disposed to learn Doctrine (at least for now).Rile
I gotta' agree with the Doctrine suggestion. It's not that difficult to pick up and it solves this whole issue rather elegantly. I never create a ZF project without Doctrine anymore.Straightedge
I have to admit, I am thoroughly underwhelmed with Zend's native models. But, since Zend is completely modular, there is no reason not to swap out the unimpressive with the well-built. Doctrine gets the +1.Jordanjordana
S
4

Here is how I, personally, implement models. I'll use a real life example: my User model.

Whenever I create a model, I use two files and two classes: the model itself (e.g. Application_Model_User) and a mapper object (e.g. Application_Model_UserMapper). The model itself obviously contains the data, methods for saving, deleting, modifying, etc. The mapper object contains methods for fetching model objects, finding objects, etc.

Here are the first few lines of the User model:

class Application_Model_User {

    protected $_id;
    protected $_name;
    protected $_passHash;
    protected $_role;
    protected $_fullName;
    protected $_email;
    protected $_created;
    protected $_salt;

    // End protected properties

For each property, I have a getter and setter method. Example for id:

/* id */

public function getId() {
    return $this->_id;
}

public function setId($value) {
    $this->_id = (int) $value;
    return $this;
}

I also use some standard "magic methods" for exposing public getters and setters (at the bottom of each model):

public function __set($name, $value) {
    $method = 'set' . $name;
    if (('mapper' == $name) || !method_exists($this, $method)) {
        throw new Exception('Invalid user property');
    }
    $this->$method($value);
}

public function __get($name) {
    $method = 'get' . $name;
    if (('mapper' == $name) || !method_exists($this, $method)) {
        throw new Exception('Invalid user property');
    }
    return $this->$method();
}

public function setOptions(array $options) {
    $methods = get_class_methods($this);
    foreach ($options as $key => $value) {
        $method = 'set' . ucfirst($key);
        if (in_array($method, $methods)) {
            $this->$method($value);
        }
    }
    return $this;
}

Example save method:

I validate inside the save() method, using exceptions when the information fails to validate.

public function save() {        
    // Validate username
    if (preg_match("/^[a-zA-Z](\w{6,15})$/", $this->_name) === 0) {
        throw new Application_Exception_UserInfoInvalid();
    }

    // etc.

    $db = Zend_Registry::get("db");

    // Below, I would check if $this->_id is null. If it is, then we need to "insert" the data into the database. If it isn't, we need to "update" the data. Use $db->insert() or $db->update(). If $this->_id is null, I might also initialize some fields like 'created' or 'salt'.
}

For the mapper object, I have at least two methods: a method that returns a query object for selecting objects, and one that executes the query, initializes and returns objects. I use this so I can manipulate the query in my controller for sorting and filtering.

EDIT

Like I said in my comments, this post: http://weierophinney.net/matthew/archives/202-Model-Infrastructure.html was the inspiration for my current Model implementation.

More options

You can also use Zend_Form to do validation, instead of rolling your own: http://weierophinney.net/matthew/archives/200-Using-Zend_Form-in-Your-Models.html. I personally don't like this option since I think that Zend_Form is awkward to use and hard to precisely control.

When most people first learn Zend Framework, they learn to subclass Zend_Db related classes. Here is an article that demonstrates this: http://akrabat.com/zend-framework/on-models-in-a-zend-framework-application/

I mentioned that I don't like doing this. Here are a few reasons why:

  • It's difficult to create models that involve derived/calculated fields (i.e. data populated from other tables)
  • I found it impossible to incorporate access control (populated from my database)
  • I like having full control over my models

EDIT 2

For your second example: You can use Zend_Paginator for this. I mentioned that, in your wrapper, you create a method that returns a database query object for selecting objects. Here's my simplified but working user mapper:

class Application_Model_UserMapper {

    public function generateSelect() {
        $db = Zend_Registry::get("db");

        $selectWhat = array(
            "users_id",
            "name",
            "role",
            "full_name",
            "email",
            "DATE_FORMAT(created, '%M %e, %Y at %l:%i:%s %p') as created",
            "salt",
            "passhash"
        );

        return $db->select()->from(array("u" => "users"), $selectWhat);
    }


    public function fetchFromSelect($select) {
        $rows = $select->query()->fetchAll();
        $results = array();

        foreach ($rows as $row) {
            $user = new Application_Model_User();

            $user->setOptions(array(
                "id" => $row["users_id"],
                "name" => $row["name"],
                "role" => $row["role"],
                "fullName" => $row["full_name"],
                "email" => $row["email"],
                "created" => $row["created"],
                "salt" => $row["salt"],
                "passHash" => $row["passhash"]
            ));

            $results[] = $user;
        }

        return $results;
    }

}

To handle the paginator, I write a custom Paginator plugin and save it to library/Application/Paginator/Adapter/Users.php. Be sure you have your appnamespace and autoloaderNamespaces[] setup correctly in application.ini. Here is the plugin:

class Application_Paginator_Adapter_Users extends Zend_Paginator_Adapter_DbSelect {
    public function getItems($offset, $itemCountPerPage) {
        // Simply inject the limit clause and return the result set
        $this->_select->limit($itemCountPerPage, $offset);
        $userMapper = new Application_Model_UserMapper();
        return $userMapper->fetchFromSelect($this->_select);
    }
}

In my controller:

// Get the base select statement
$userMapper = new Application_Model_UserMapper();
$select = $userMapper->generateSelect();

// Create our custom paginator instance
$paginator = new Zend_Paginator(new Application_Paginator_Adapter_Users($select));

// Set the current page of results and per page count
$paginator->setCurrentPageNumber($this->_request->getParam("page"));
$paginator->setItemCountPerPage(25);

$this->view->usersPaginator = $paginator;

Then render the paginator in your view script.

Samovar answered 21/8, 2011 at 19:29 Comment(3)
Thank you very much for your answer! I'll start a bounty for this answer for more possible implementation of models, but anyway I appreciate your answer, thanks!Rile
@s3v3n: Glad it helped! Here is the blog post that inspired how I currently do models: weierophinney.net/matthew/archives/…; One difference is that the author of that post calls the mapper a "gateway"Samovar
Thanks :) You're using magic methods, this gave me an idea and I started some implementation. I'll post mine here to (I don't know if is possible to offer my own bounty to myself, but I will NOT do that in any case :) ).Rile
A
2

I do something similar to SimpleCode's way. My style derives from Pádraic Brady. He has multiple blog posts but the best and quickest resource of his is a online book he wrote: Survive the Deep End!. This link should take you straight to his chapter on Models, Data Mappers, and other cool goodies such as Lazy Loading. The idea is the following:

You have entities such as a User with The properties are defined in an array. All your entities extend an abstract class with magic getter/setters that get from or update this array.

class User extends Entity
{
    protected $_data = array(
        'user_id' => 0,
        'first_name' => null,
        'last_name' => null
    );
}

class Car extends Entity
{
    protected $_data = array(
        'car_id' => 0,
        'make' => null,
        'model' => null
    );
}

class Entity
{
    public function __construct($data)
    {
        if(is_array($data))
        {
            $this->setOptions($data);
        }
    }

    public function __get($key)
    {
        if(array_key_exists($key, $this->_data)
        {
            return $this->_data[$key];
        }

        throw new Exception("Key {$key} not found.");
    }

    public function __set($key, $value)
    {
        if(array_key_exists($key, $this->_data))
        {
            $this->_data[$key] = $value;
        }

        throw new Exception("Key {$key} not found.");
    }

    public function setOptions($data)
    {
        if(is_array($data))
        {   
            foreach($data as $key => $value)
            {
                $this->__set($key, $value);
            }
        }
    }

    public function toArray()
    {
        return $this->_data;
    }
}

$user = new User();
$user->first_name = 'Joey';
$user->last_name = 'Rivera';

echo $user->first_name; // Joey

$car = new Car(array('make' => 'chevy', 'model' => 'corvette'));
echo $car->model; // corvette

Data Mappers to me are separate from the Entities, their job is to do the CRUD (create, read, update, and delete) to the db. So, if we need to load an entity from the db, I call a mapper specific to that entity to load it. For example:

<?php

class UserMapper
{
    $_db_table_name = 'UserTable';
    $_model_name = 'User';

    public function find($id)
    {
        // validate id first

        $table = new $this->_db_table_name();
        $rows = $table->find($id);

        // make sure you get data

        $row = $rows[0]; // pretty sure it returns a collection even if you search for one id
        $user = new $this->_model_name($row); // this works if the naming convention matches the user and db table
        //else
        $user = new $this->_model_name();

        foreach($row as $key => $value)
        {
            $user->$key = $value;
        }

        return $user;
    }
}

$mapper = new UserMapper();
$user = $mapper->find(1); // assuming the user in the previous example was id 1
echo $user->first_name; // Joey

This code is to give an idea of how to architect the code in this way. I didn't test this so I may have created some typos/syntax errors as I wrote it. Like others have mentioned, Zend lets you do what you want with Models, there is no right and wrong it's really up to you. I usually create a table class for every table in the db that I want to work with. So if I have a user table, I usually have a User entity, User Mapper, and a User Table class. The UserTable would extend Zend_Db_Table_Abstract and depending on what I'm doing won't have any methods inside or sometimes I'll overwrite methods like insert or delete depending on my needs. I end up with lots of files but I believe the separation of code makes it much easier to quickly get to where I need to be to add more functionality or fix bug since I know where all the parts of the code would be.

Hope this helps.

Aftercare answered 25/8, 2011 at 14:11 Comment(1)
Creating a base model class with shared boilerplate is a great idea. Just applied it to my own project; thanks!Samovar
S
0

Folder Structure

application
--models
----DbTable
------User.php
--controllers
----IndexController.php
--forms
----User.php
--views
----scripts
------index
--------index.phtml

application/models/DbTable/User.php

class Application_Model_DbTable_User extends Zend_Db_Table_Abstract
{
    protected $_name = 'users';
    protected $_primary = 'user_id';
}

application/forms/User.php

class Form_User extends Zend_Form
{
    public function init()
    {       
        $this->setAction('')
            ->setMethod('post');

        $user_name = new Zend_Form_Element_Text('user_name');
        $user_name->setLabel("Name")->setRequired(true);

        $user_password = new Zend_Form_Element_Text('user_password');
        $user_password->setLabel("Password")->setRequired(true);

        $submit = new Zend_Form_Element_Submit('submit');
        $submit->setLabel('Save');

        $this->addElements(array(
            $user_name,
            $user_password,
            $submit
        ));
    }
}

application/controllers/IndexController.php

class IndexController extends Zend_Controller_Action
{
    public function init()
    {

    }

    public function indexAction()
    {
        $form = new Form_User();
        if($this->getRequest()->isPost() && $form->isValid($this->getRequest()->getPost()))
        {
            $post = $this->getRequest()->getPost();
            unlink($post['submit']);

            $ut = new Application_Model_DbTable_User();
            if($id = $ut->insert($post))
            {
                $this->view->message = "User added with id {$id}";
            } else {
                $this->view->message = "Sorry! Failed to add user";
            }
        }
        $this->view->form = $form;
    }
}

application/views/scripts/index/index.phtml

echo $this->message;
echo $this->form;
Stollings answered 26/8, 2011 at 18:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.