5
votes

Here is the deal. Using Doctrine ORM for PHP and I need to "decouple" model from entity persistent layer. Say we have UserEntity which holds all the pretty stuff for db mapping, as: annotations, properties, setters/getters and so on. From the other hand i'd like to have a separate User class which only holds business related logic, for instance: User::getFullName(). Furthermore i want User to extend UserEntity so User inherits all the access methods.

Possible solutions i've checked through don't work for me:

  • just extending model from entity and then specifying model in DQL does not work
  • make UserEntity /** @MappedSuperclass */ does not work since in this case UserEntity "is not itself an entity"
  • InheritanceType / DiscriminatorColumn / DiscriminatorMap does not work as well cause model is not an entity

any ideas ?

4
Go the opposite way. UserEntity extends User. User should not need nor want access to all the UserEntity access properties. Instead it should only have your business methods like changeName.Cerad
no, User has to have access to UserEntity. e.g. User::getFullName() is return $this->firstName . $this->lastName. in this case firstName / lastName need to be accessible from the User level.Sergey Poskachey
Yes, the MappedSuperclass and InheritaceType annotations are all database related behaviors, they won't help you achieve what you want.Mihai Stancu

4 Answers

3
votes

Gotcha ! (>= PHP 5.4 solution)

Short: make entity a trait, use entity trait in model class. Checkout this doc page: http://doctrine-orm.readthedocs.org/en/latest/tutorials/override-field-association-mappings-in-subclasses.html.

Example: Suppose we have user model. First create user entity:

/**
 * @ORM\Table(name="user")
 */
trait UserEntity {

     /**
      * @var integer
      *
      * @ORM\Column(name="id", type="integer", nullable=false)
      * @ORM\Id
      * @ORM\GeneratedValue(strategy="IDENTITY")
      */
      protected $id;

     /**
      * @var string
      *
      * @ORM\Column(name="first_name", type="string", length=100, nullable=true)
      */
      protected $firstName;

     /**
      * @var string
      *
      * @ORM\Column(name="last_name", type="string", length=100, nullable=true)
      */
      protected $lastName;

      // other mapping here...
}

Then create a model and use an entity trait in it:

/**
 * @ORM\Entity
 */
class User {
    use UserEntity;

    public function getFullName() {
        return $this->firstName . ' ' . $this->lastName;
    }
}

Mind @Entity annotation in User model. This is required in order to use model directly in entity manager.

Now suppose we need an Admin model which extends User one. This is a bit tricky. We have to change @Entity to @MappedSuperclass in User so it can be extended. Then create Admin model, declare it as @Entity and also redeclare a table name on it using @Table annotation (otherwise Doctrine will be confused from which table to fetch for some reason). Look like this:

/**
 * @ORM\MappedSuperclass
 */
class User {
    use UserEntity;

    public function getFullName() {
        return $this->firstName . ' ' . $this->lastName;
    }
}

/**
 * @ORM\Entity
 * @ORM\Table(name="user")
 */
class Admin extends User {
    public function getFullName() {
        return parent::getFullName() . ' (admin)';
    }
}

This way both User and Admin models (=entities) can be used in Doctrine.

And no we can do whatever we would normally do with entities: find() them via entity manager, use models directly in queries, etc.

0
votes

Hey kids wanna buy some magic?

You could use magic methods in PHP to forward all of the accessor method calls such as:

class UserModel
{
    protected $userEntity;

    public function __construct($entity)
    {
        $this->userEntity = $entity;
    }

    public function __call($name, $arguments)
    {
        if (!method_exists($this, $name) AND method_exists($this->userEntity, $name)) {
            return call_user_func_array(array($this, $name), $arguments);
        }
    }
}

You can define your own local EntityRepository class which extends the Doctrine\ORM\EntityRepository. This way you can ensure that the find, findBy, findOneBy methods will create new instances of your Model, set the $entity object inside the Model and return the new Model instance instead of the Entity.

Want a cleaner integration with Doctrine result outputs?

You can write a model class (without registering it into Doctrine2) which extends the entity class and write your own custom Hydrator class (and register it into Doctrine2) to make sure your results will be returned as an array of the Model class not an array of the Entity class.

Wanna do it the Symfony way?

You could define all of those "db-related business logic" inside your entities repository class and all of those "non-db-related business logic" inside a service class (you could create a service dedicated to one entity if you wanted to). And then call your business logic layer (services, repositories) with db-logic parameters (entities).

On the other hand, the Symfony way is very flexible since you can build any functionality outside of the pre-established Symfony structure as long as they're PSR.

0
votes

About custom hydrator. In short, this solution is also not an option for me (i might be missing anything though). two pitfalls here:

  1. not convenient to remap from raw db data into model object inside custom hydrator. the problem is that fields in db table are 'single words' (like: firstname / lastname) while as in objects i manually modified them to be camel cased (for readability). this of course can be solved reading meta info from entity's annotations but this will involve magic and extra timing resources. so this solution would be quite 'dirty'.
  2. how would i use custom hydrator in e.g. 'find' functions ? like: $em->find('My\Model\User', 1) ? Because i want my models to be hundred percent usable in every place entity would be used.

So as a result, no solution suggested by Mihai Stancu is working for me.

  • Magic inside model class: not a clean solution because it involves passing entity to the constructor and won't give you type hinting (cause of the magic __call )
  • Custom hydrator: seems to be more clean but can't be used in all cases and remapping from db raw result into model object is complex (=time consuming)
  • Symfony's service way: also not clean solution because i need 100% separation of model logic and db related logic (so not a single mapping information / annotation in my model, just a pure business logic)
0
votes

Well, I have been askin myself that question lately, and the simple answer I have come to is to move all anotations out of the entity into yaml or xml to declutter my "model"/entity.

Then I have all my properties in my user class, but all the persistence configuration has been moved out, then you can also do whatever you want with the getters and setters(in my opinion remove as many as possible).