My problem is exactly that described in the Strategy Pattern article in Doctrine documentation :
A Page entity
A page can have some blocks
A block might be a text, an image, a form, a calendar, ... (strategy)
A page knows about blocks it contains, but doesn't know about their behaviours
Block inheritance is not possible
Described solution (Strategy pattern) seems exactly what I need (read article for further information) :
Page:
<?php
namespace Page\Entity;
class Page
{
/**
* #var int
* #Id #GeneratedValue
* #Column(type="integer")
*/
protected $id;
/**
* #var string
* #Column
*/
protected $title;
/**
* #var string
* #Column(type="text")
*/
protected $body;
/**
* #var Collection
* #OneToMany(targetEntity="Block", mappedBy="page")
*/
protected $blocks;
// ...
}
Block:
<?php
namespace Page\Entity;
class Block
{
/**
* #Id #GeneratedValue
* #Column(type="integer")
*/
protected $id;
/**
* #ManyToOne(targetEntity="Page", inversedBy="blocks")
*/
protected $page;
/**
* #Column
*/
protected $strategyClass;
/**
* Strategy object is instancied on postLoad by the BlockListener
*
* #var BlockStrategyInterface
*/
protected $strategyInstance;
// ...
}
Strategy interface:
<?php
namespace Page\BlockStrategy;
interface BlockStrategyInterface
{
public function setView($view);
public function getView();
public function setBlock(Block $block);
public function getBlock();
public function renderFrontend();
public function renderBackend();
}
I can easily imagine what would be my strategy if I would display a form or a calendar;
but what if my strategy is to display content of an other entity ?
The block needs to know about entity class/id and has to be deleted when related entity is removed.
I imagined to add entityClass and entityId properties in Block and load related entity on the postLoad event in a BlockListener.
But what if related entity doesn't exist ? I can't remove the block in postLoad.
So, I imagined to create an other listener watching for removal of related entity and remove refering Block in that listener.
But it means that I need to add a listener for every entity that can be put in a block.
It could work, but it seems very complicated... maybe someone has a better idea ?
I'm not sure if I understood well your question, but if what you want is to have entities inside entities and delete the child entities when the father is deleted, you could also treat the entity as a block and use a Composite pattern.
You basically could use the same interface on the entity and the blocks, and on the entity the display function could be something like:
foreach ($block in $blocks) {
$block->display();
}
For deleting all the children when you delete the parent entity, you could simply do it on the destructor of the entity.
function __destruct() {
foreach ($block in $blocks) {
/* call a common interface function that does all that need to be done, implemented on each block */
}
}
For more on Composite pattern:
http://en.wikipedia.org/wiki/Composite_pattern
Related
I have the main entity
/**
* #ORM\Entity()
*/
class Document
{
/**
* #var int
* #ORM\Id()
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var DocumentStatus
* #ORM\ManyToOne(targetEntity="DocumentStatus")
*/
private $status;
/**
* #var string
* #ORM\Column(type="text")
*/
private $text;
}
and the lookup "enum" entity (seeding on application deploy)
/**
* #ORM\Entity(repositoryClass="DocumentStatusRepository");
*/
class DocumentStatus
{
const DRAFT = 'draft';
const PENDING = 'pending';
const APPROVED = 'approved';
const DECLINED = 'declined';
/**
* #var int Surrogate primary key
* #ORM\Id()
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string Natural primary key (name for developers)
* #ORM\Column(type="string", unique=true)
*/
private $key;
/**
* #var string Short name for users
* #ORM\Column(type="string", unique=true)
*/
private $name;
/**
* #var string Full decription for users
* #ORM\Column(type="string", nullable=true, unique=true)
*/
private $description;
}
with simple repository
class DocumentStatusRepository extends EntityRepository
{
public function findOneByKey($key)
{
return parent::findOneBy(['key' => $key]);
}
}
I want to encapsulate domain logic of document lifecycle by intoducing methods like
public function __construct($text)
{
$this->text = $text;
$this->status = $something->getByKey(DocumentStatus::DRAFT);
}
public function approve()
{
try {
$this->doSomeDomainActions();
$this->status = $something->getByKey(DocumentSatus::DRAFT);
} catch (SomeDomainException($e)) {
throw new DocumentApproveException($e);
}
}
...
or
public function __construct($text)
{
$this->text = $text;
$this->status = $something->getDraftDocumentStatus()
}
public function approve()
{
$this->status = $something->getApprovedDocumentStatus()
}
...
without public setters. Also I want keep Document loose coupling and testable.
I see the next ways:
Permanent inject DocumentStatusRepository (or service that encapsulates it) into every instance by constructor on entity creation and by using public setter in postLoad subscriber
Permanent inject DocumentStatusRepository by static Document method on application bootstrap or on loadClassMetadata
Temporary inject DocumentStatusRepository in constructor and methods like Document::approve
Use setStatus() method with complex logic based on $status->key value
Encapsulate document domain logic in some DocumentManager and use Document entity like simple data storgae or DTO :(
Are there other ways? Which way is easier and more convenient to use in the long term?
Using generated Identities for Document.
Now you generate the identity on the side of the database. So you save Document from the domain perspective in inconsistent state. Entity/Aggregate should be identified, if it has no id it shouldn't exists.
If you really want to keep to database serials, add method to the repository which will generate id for you.
Better way is to use uuid generator for example ramsey/uuid.
And inject the id to the constructor.
DocumentStatus as Value Object
Why Document Status is Entity? It does look like a simple Value Object.
Then you can use Embeddable annotation. So it will leave within same table in the database, no need for doing inner joins.
DocumentStatus gets behaviour for example ->draftDocumentStatus(), which returns NEW DocumentStatus with draft status, so you can switch the old instance one with new one. ORM will do the rest.
DocumentStatusRepository
If you really want to keep DocumentStatus as entity, which in my opinion is wrong you shouldn't have DocumentStatusRepository.
Document is your aggregate root and the only entrance to the DocumentStatus, should be by aggregate root.
So you will have DocumentRepository only, which will be responsible for rebuilding the whole aggregate and saving it.
Also you should change the mapping.
It should have FETCH=EAGER type, so it will retrieve DocumentStatus with Document together.
Secondly you should do mapping with CASCADE=ALL and ORPHANREMOVAL=TRUE.
Otherwise, if you remove Document, DocumentStatus will stay in the database.
I've an entity Order, with a property events which should contain a list of all changes made to this entity.
The Order class:
<?php
/**
* #ORM\Entity
*/
class Order
{
// more stuff...
/**
* #ORM\OneToMany(
* targetEntity="OrderEvent",
* mappedBy="order",
* cascade={"persist", "merge"}
* )
*/
protected $events;
// more stuff...
}
The OrderEvent class:
/**
* #ORM\Entity
*/
class OrderEvent
{
// more stuff...
/**
* #ORM\ManyToOne(targetEntity="Order", inversedBy="events")
* #ORM\JoinColumn(nullable=false)
*/
protected $order;
// more stuff...
}
class OrderLifecycle
{
public function preUpdate(Order $order, PreUpdateEventArgs $args)
{
$changes = $args->getEntityChangeSet();
if (!empty($changes)) {
$event = new OrderEvent();
$event->setOrder($order)
->setChanges($changes);
$order->addEvent($event);
return $event;
}
}
}
But according to the doctrine documentation, the preUpdate method should not be used to change associations.
What is the recommended way to do things like this one?
I am using Zend Framework 2, but I think that's not relevant.
I think in this case you could use PostUpdate event. In that case you are sure that the update action was successful and you can do what you want; add the new OrderEvent instance to your $events collection.
EDIT
You are not the first one implementing such thing. Maybe you should check existing examples and see how they deal with this (or even consider using it). For example the Gedmo Loggable solution.
With this extension you can mark entities as loggable with a simple #annotiation:
/**
* #Entity
* #Gedmo\Loggable
*/
class Order
{
// Your class definition
}
I have a problem with lazy loading in symfony2/doctrine2.
I have a normal object (for example: type item) and this object has an id. If I look at the object at runtime I see that the id is set. Every other parameters like icon and amount are empty. I know, this is how lazy loading works but when I call the getters (getIcon) nothing happens. The icon attribute is still empty. I also tried to call the __load method but it doesn't help.
Sorry, forgot the code
class Character {
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="Entity\Item", mappedBy="character")
*/
protected $item;
/*********************************************************************
* Custom methods
*/
public function getItem() {
return $this->item;
}
}
And this is the object where the lazy loading not works.
class Item {
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="integer")
*/
protected $amount;
/**
* #ORM\Column(type="string")
*/
protected $icon;
}
EDIT2:
Constructor of character class
public function __construct()
{
$this->item = new \Doctrine\Common\Collections\ArrayCollection();
}
So what the previos comments to your initial post are pointing at, is, that you need to implemend a ManyToOne relation in your Item entity to get all your stuff working.
In yout Character Entity you have this lines of code
/**
* #ORM\OneToMany(targetEntity="Entity\Item", mappedBy="character")
*/
protected $item;
This says you have a relation to an Entity Item which mappes the relation in the attribute "character". In this attribute the relation is stored. If you look into the database, you won't find any stored relations, because you class Item does not have the described mapping attribute character. Like gp_sflover pointed out, a OneToMany relations needs to be Bidirectional an required a ManyToOne relation in the "owning" side. So what you have to do is, add the following code to your Item Entity
/**
* #ORM\ManyToOne(targetEntity="Entity\Character", inversedBy="item")
*/
protected $character;
The inversedBy attribute creates a bidirectional relation. Without this statement, you wouldn't be able to load getItems from your Character entity.
If you have changed your code you have to update your database and to restore the elements. After this, everything will work fine.
I have a question about Doctrine 2 and the ability (or not?) to extend an association between to classes.
Best explained with an example :)
Let's say I have this model (code is not complete):
/**
* #Entity
*/
class User {
/**
* #ManyToMany(targetEntity="Group")
* #var Group[]
*/
protected $groups;
}
/**
* #Entity
*/
class Group {
/**
* #ManyToMany(targetEntity="Role")
* #var Role[]
*/
protected $roles;
}
/**
* #Entity
*/
class Role {
/**
* #ManyToOne(targetEntity="RoleType")
* #var RoleType
*/
protected $type;
}
/**
* #Entity
*/
class RoleType {
public function setCustomDatas(array $params) {
// do some stuff. Default to nothing
}
}
Now I use this model in some projects. Suddenly, in a new project, I need to have a RoleType slightly different, with some other fields in DB and other methods. Of course, it was totally unexpected.
What I do in the "view-controller-but-not-model" code is using services:
// basic configuration
$services['RoleType'] = function() {
return new RoleType();
};
// and then in the script that create a role
$role_type = $services['RoleType'];
$role_type->setCustomDatas($some_params);
During application initialization, I simply add this line to overwrite the default RoleType
$services['RoleType'] = function() {
return new GreatRoleType();
};
Ok, great! I can customize the RoleType call and then load some custom classes that do custom things.
But... now I have my model. The model says that a Role targets a RoleType. And this is hard-written. Right now, to have my custom changes working, I need to extend the Role class this way:
/**
* #Entity
*/
class GreatRole extends Role {
/**
* Changing the targetEntity to load my custom type for the role
* #ManyToOne(targetEntity="GreatRoleType")
* #var RoleType
*/
protected $type;
}
But then, I need to extend the Group class to target GreatRole instead of Role.
And in the end, I need to extend User to target GreatGroup (which targets GreatRole, which targets GreatRoleType).
Is there a way to avoid this cascade of extends? Or is there a best practice out there that is totally different from what I did?
Do I need to use MappedSuperClasses? The doc isn't very explicit...
Thanks for your help!
--------------------------- EDIT ---------------------------
If I try to fetch all the hierarchy from User, that's when I encounter problems:
$query
->from('User', 'u')
->leftJoin('u.groups', 'g')
->leftJoin('g.roles', 'r')
->leftJoin('r.type', 't');
If I want to have a "r.type" = GreatRoleType, I need to redefine each classes.
To make it simple, let's say I have two objects with one-to-many relation:
User --(1:n)--> Request
with User defined as
class User {
...
/** #OneToMany(targetEntity="Request", mappedBy="user", cascade={"all"}) */
private $request;
...
}
and Request defined as
class Request {
...
/** #ManyToOne(targetEntity="User", inversedBy="request", cascade={"persist"}) */
private $user;
...
}
Is it possible to create a method that removes all Requests associated with User from within User entity?
What I need is something like this:
class User {
....
public function removeAllMyRequests() {
foreach ($this->getAllMyRequests() as $req)
$this->em->remove($req);
}
....
}
But apparently I'm not supposed to invoke entity manager from within entity.
You can mark the association with "Orphan Removal":
/**
* #Entity
*/
class User
{
/**
* #OneToMany(
* targetEntity="Request",
* mappedBy="user",
* cascade={"all"},
* orphanRemoval=true
* )
*/
private $requests;
}
Any Request object removed from the User#requests collection will be marked for removal during the next EntityManager#flush() call.
To remove all items at once, you can simply use Doctrine\Common\Collections\Collection#clear():
public function removeAllMyRequests() {
$this->requests->clear();
}
I think you are looking for the "cascade" option : http://docs.doctrine-project.org/en/2.0.x/reference/working-with-associations.html#transitive-persistence-cascade-operations