zf2 - creating models (with dependencies) in mappe

2019-09-13 17:25发布

问题:

Following from my previous post about removing ServiceLocatorAwareInterface's from my zf2 app, i am now faced with a puzzle involving object creation when using data mappers.

The current implementation of my data mapper uses a tablegateway to find specific rows, calls the service manager to obtain a domain object, then populates and returns the full object.

public function findById($userId){
        $rowset = $this->gateway->select(array('id' => $userId));
        $row = $rowset->current();
        if (!$row) {
            throw new \DomainException("Could not find user with id of $userId in the database");
        }
        $user = $this->createUser($row);
        return $user;
    }

public function createUser($data){
        $userModel = $this->getServiceManager()->get('Model\User');
        $hydrator = $this->getHydrator();
        if($data instanceof \ArrayObject){
            $hydrator->hydrate($data->getArrayCopy(), $userModel);
        }else{
            $hydrator->hydrate($data, $userModel);
        }
        return $userModel;
    }

The model needs to be called from the service manager because it has other dependencies, so calling $user = new App\Model\User() from within the mapper is not an option.

However, now i am removing instances of the servicemanager from my code, i am unsure of the best way to get the model into the mapper. The obvious answer is to pass it in the constructor and save the instance as a property of the mapper:

 public function __construct(TableGateway $gateway, \App\Model\User $userModel){
        $this->_gateway = $gateway;
        $this->_userModel= $userModel;
    }

    public function createUser($data){
        $userModel = $this->_userModel;
        //....snip....
    }

This works to a degree, but then multiple calls to createUser (such as when finding all users, for instance) over writes each instance with the last objects data (as to be expected, but not what i want)

So i need a "new" object returned each time i call createUser, but the dependency being passed into the constructor. With the model passed into the constructor I can clone the object eg.

    public function createUser($data){
        $userModel = clone $this->_userModel
        //....snip....
    }

...but something about it doesn't seem right, code smell?

回答1:

You are right, it doesn't smell good.

Designing an ORM isn't easy. There is and probably always will be discussion about the way an ORM should be designed. Now, when I'm trying to understand your design I noticed you are pointing out that your models contain the data but also have "other" dependencies. This is wrong, the models containing your data should work without any layer in your application.

Entities should work without the ORM

In my opinion you should separate your business logic (dependencies) from your data. This will have many advantages:

  • More expressive
  • Easier to test
  • Less coupling
  • More flexible
  • Easier to refactor

For more information about how to design your ORM layer I highly recommend browsing through these slides.

DataMaper

Lets make the UserMapper responsible for separating the in-memory objects (containing only data) from the database.

class UserMapper
{
    protected $gateway;
    protected $hydrator;        

    public function __construct(TableGateway $gateway, HydratorInterface $hydrator)
    {
        $this->gateway = $gateway;
        $this->hydrator = $hydrator;
    }

    public function findOneById($id)
    {
        $rowset = $this->_gateway->select(array('id' => $id));
        $row = $rowset->current();

        if(!$row) {
            throw new \DomainException("Could not find user with id of $id in the database.");
        }
        $user = new User;
        $this->hydrator->hydrate($row, $user);
        return $user;
    }

    public function findManyBy(array $criteria)
    {
       // $criteria would be array('colum_name' => 'value')
    }

    public function save(User $user)
    {
        $data = $this->hydrator->extract($user);
        // ... and save it using the $gateway.
    }
}

For more information about the responsibility of data mappers check out Martin Fowler's definition.

Buniness Logic

It's recommended not to place any model related business logic directly into the Controller. Therefor lets just create a simple UserService which will handle validation. If your fond of form objects you could also use Zend\Form\Form in this process.

class UserService
{
    protected $inputFilter;
    protected $hydrator;

    public function __construct(InputFilter $inputFilter, HydratorInterface $hydrator)
    {
        $this->inputFilter = $inputFilter;
        $this->hydrator = $hydrator;
    }

    protected function validate(array $data)
    {
        // Use the input filter to validate the data;
    }

    public function createUser(array $data)
    {
        $validData = $this->validate($data);
        $user = new User;
        $this->hydrator->hydrate($validData, $user);
        return $user;
    }
}

Data Object

Now lets make the objects containing the data Plain Old PHP Objects, not bound by any restriction. This means they are not coupled with any logic and we could use them anywhere. For instance if we decide to replace our ORM with another like Doctrine.

class User
{
    protected $name;

    public function setName($name)
    {
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }
}

More information about the concept of Plain Old PHP Objects can be found on Wikipedia's explanation of POJO.