I've a class, Proposal, which has a $status of type ProposalStatus. Now, for the most part the ProposalStatus's identity does not change... but it does (sort of). It's $id is fixed, but the $display_name and $definition (just strings) can change over a long period of time as the master data is updated, but it will NOT change within the lifetime of an HTTP Request-Response.
Question #1 - Entity or Value Object?
Is a value object something is never supposed to change or only never supposed to change over the lifetime of a specific execution of the application? If the display name or definition are changed then really I expect / want it to be changed for everyone. However, since they can be defined outside of the proposal I'm think that just straight up makes them entities instead of value objects.
At no time does the Proposal change the ProposalStatus's attributes, it only changes which ProposalStatus is has.
Question #2 - How to set the status correctly for a domain-driven design?
My proposal object has the ability to manage it's statuses, but in order to do that I need to have a specific ProposalStatus object. Only, where is the list of statuses that allows it to returns the right expected to be?
I could get it from a the ProposalRepository... but everything is accessed via the aggregate root which the Proposal so that doesn't make sense.
I could have constants that match the $id of the ProposalStatus, but that seems wrong.
I could make a ProposalStatusRepository... but should I be accessing another repository from within the Proposal?
I could make a array of all possible statuses with the $id as the key and add to the proposal, but that isn't much different from a repository...
Example:
class ProposalStatus {
protected $id; // E.g., pending_customer_approval
protected $display_name; // E.g., Pending Customer Approval
protected $definition; // E.g., The proposal needs to be approved by the customer
}
class Proposal {
/**
* The current status of the proposal
* #var ProposalStatus
*/
protected $proposal_status;
public function withdraw() {
// verify status is not closed or canceled
// change status to draft
}
public function submit() {
// verify status is draft
// change status to pending customer approval
}
public function approve() {
// verify status is pending customer approval
// change status to approved
}
public function reject() {
// verify status is pending customer approval
// change status to rejected
}
public function close() {
// verify status is not canceled
// change status to closed
}
public function cancel() {
// verify status is not closed
// change status to canceled
}
}
From what I understand from your domain, ProposalStatus should be a Value object. So, it should be made immutable and contain specific behavior. In your case, the behavior is testing for a specific value and initializing only to permitted range of values. You could use a PHP class, with a private constructor and static factory methods.
/**
* ProposalStatus is a Value Object
*/
class ProposalStatus
{
private const DRAFT = 1;
private const PENDING_CUSTOMER_APPROVAL = 2;
private const CANCELLED = 3;
private const CLOSED = 4;
/** #var int */
private $primitiveStatus;
private function __construct(int $primitiveStatus)
{
$this->primitiveStatus = $primitiveStatus;
}
private function equals(self $another): bool
{
return $this->primitiveStatus === $another->primitiveStatus;
}
public static function draft(): self
{
return new static(self::DRAFT);
}
public function isDraft(): bool
{
return $this->equals(static::draft());
}
public static function pendingCustomerApproval(): self
{
return new static(self::PENDING_CUSTOMER_APPROVAL);
}
public function isPendingCustomerApproval(): bool
{
return $this->equals(static::pendingCustomerApproval());
}
public static function cancelled(): self
{
return new static(static::CANCELLED);
}
public function isCancelled(): bool
{
return $this->equals(static::cancelled());
}
public static function closed(): self
{
return new static(static::CLOSED);
}
public function isClosed(): bool
{
return $this->equals(static::closed());
}
}
class Proposal
{
/** #var ProposalStatus */
private $status;
public function __construct()
{
$this->status = ProposalStatus::draft();
}
public function withdraw()
{
if (!$this->status->isClosed() && !$this->status->isCancelled()) {
$this->status = ProposalStatus::draft();
}
}
// and so on...
}
Note that immutability is an important characteristic of a Value object.
In case that your ProposalStatus is a fixed list of values just go for the enumeration approach.
Otherwise you need to treat ProposalStatus as an AggregateRoot that users can create, update and delete (I guess). When assigning a ProposalStatus to a Proposal you just need the ID. If you want to check that the given ID exists you just need to satisfy the invariant with a specialized query. Specification pattern fits well here.
class ProposalStatusExistsSpecification
{
public function isSatisfiedBy(string $proposalSatusId): bool
{
//database query to see if the given ID exists
}
}
You can find here the Interfaces to implement your specification.
Is list of all possible proposal statuses static? I think it is. So ProposalStatus looks like a simple enumeration. Attributes like DisplayName and Definition are not related to business code.
Just define ProposalStatus as enumeration (static class with read-only fields or any other structure supported by your language). It shuld be defined in business layer. Bussiness code should be able to distinguish enumeration values (e.g. if (proposal.Status == ProposalStatus.Pending) { poposal.Status = ProposalStatus.Approved; }).
In application or even presentation layer define a dictionary that contains DisplayName and Definition mapped to ProposalStatus. It will be used only when displaying data to users.
Related
This is my demonstration case:
<?php
declare(strict_types=1);
final class Order
{
/** #var array|OrderPosition[] */
private $orderPositions;
public static function fromArray($orderData)
{
assert(isset($orderData['orderId']));
assert(isset($orderData['positions']));
return
new self(
$orderData['orderId'],
array_map(
function (array $positionData): OrderPosition {
// I would like to put the "future self (Order)" alread here
return OrderPosition::fromArray($positionData);
},
$orderData['positions']
)
);
}
private function __construct(string $orderId, array $orderPositions)
{
$this->orderPositions = $orderPositions;
// what I want to avoid is:
array_walk($orderPositions, function (OrderPosition $position) {
$position->defineOwningOrder($this);
});
}
}
final class OrderPosition
{
/** #var Order */
private $owningOrder;
public static function fromArray($positionData /* + as mentioned I'd like to put the "calling" Order here already...? */)
{
return
new self(
$positionData['productId'],
$positionData['amount']
);
}
private function __construct(string $productId, int $amount)
{
// …
}
/** #internal */
public function defineOwningOrder(Order $order)
{
$this->owningOrder = $order;
}
}
I like to have a pointer to the "parent"/owning Order item in my OrderPosition; however since Order is considered an Aggregate Root I want Order to be in charge of creating the collection of OrderPositions.
How should I put the Order item in every OrderPosition when the final Order is not yet there on creation?
My current approach is to late-set it in Order's ctor but that would mutate OrderPosition, strictly spoken.
You have a combination of several design decisions here which are in conflict:
immutable objects
a circular reference
a constructor which is not responsible for constructing the dependent objects
a factory method which is not allowed to see and mutate a partial object
As you say, your current implementation compromises on (1) by allowing the OrderPosition to have the extra reference added in later.
You can make the problem go away if you remove (2). What is the situation where you would have a reference to an OrderPosition and want to navigate to the Order to which it belongs? Can that situation be re-framed as a responsibility of the Order, removing the circular reference?
You could change (3) such that the constructor took the information to create OrderPositions, not the OrderPositions themselves. In your example, this would be trivial, but if in practise you have a number of different factories feeding into one constructor, this might become messy.
Alternatively, if you relax (4) you could pass the partially constructed object into the OrderPosition constructor / factory:
public static function fromArray($orderData)
{
assert(isset($orderData['orderId']));
assert(isset($orderData['positions']));
$instance = new self($orderData['orderId']);
foreach ( $orderData['positions'] as $positionData ) {
$instance->orderPositions[] = OrderPosition::fromArray($positionData, $instance);
}
return $instance;
}
While this still looks like mutation, it is only the same mutation you would do in any constructor - you are creating the initial state of the object.
In a language that supported overloaded or named constructors, fromArray would be a constructor, and might not share any implementation with other constructors. In PHP, you can emulate that pattern with an empty private function __construct(){} and static methods starting with $instance = new self;
In applying the Data Mapper pattern, the model (Domain Model in my case) is responsible for business logic where possible, rather than the mapper that saves the entity to the database.
Does it seem reasonable to build a separate business logic validator for processing user-provided data outside of the model?
An example is below, in PHP syntax.
Let's say we have an entity $person. Let's say that that entity has a property surname which can not be empty when saved.
The user has entered an illegal empty value for surname. Since the model is responsible for encapsulating business logic, I'd expect $person->surname = $surname; to somehow say that the operation was not successful when the user-entered $surname is an empty string.
It seems to me that $person should throw an exception if we attempt to fill one of its properties with an illegal value.
However, from what I've read on exceptions "A user entering 'bad' input is not an exception: it's to be expected." The implication is to not rely on exceptions to validate user data.
How would you suggest approaching this problem, with the balance between letting the Domain Model define business logic, yet not relying on exceptions being thrown by that Domain Model when filling it with user-entered data?
A Domain Model is not necessarily an object that can be directly translated to a database row.
Your Person example does fit this description, and I like to call such an object an Entity (adopted from the Doctrine 2 ORM).
But, like Martin Fowler describes, a Domain Model is any object that incorporates both behavior and data.
a strict solution
Here's a quite strict solution to the problem you describe:
Say your Person Domain Model (or Entity) must have a first name and last name, and optionally a maiden name. These must be strings, but for simplicity may contain any character.
You want to enforce that whenever such a Person exists, these prerequisites are met. The class would look like this:
class Person
{
/**
* #var string
*/
protected $firstname;
/**
* #var string
*/
protected $lastname;
/**
* #var string|null
*/
protected $maidenname;
/**
* #param string $firstname
* #param string $lastname
* #param string|null $maidenname
*/
public function __construct($firstname, $lastname, $maidenname = null)
{
$this->setFirstname($firstname);
$this->setLastname($lastname);
$this->setMaidenname($maidenname);
}
/**
* #param string $firstname
*/
public function setFirstname($firstname)
{
if (!is_string($firstname)) {
throw new InvalidArgumentException('Must be a string');
}
$this->firstname = $firstname;
}
/**
* #return string
*/
public function getFirstname()
{
return $this->firstname;
}
/**
* #param string $lastname
*/
public function setLastname($lastname)
{
if (!is_string($lastname)) {
throw new InvalidArgumentException('Must be a string');
}
$this->lastname = $lastname;
}
/**
* #return string
*/
public function getLastname()
{
return $this->lastname;
}
/**
* #param string|null $maidenname
*/
public function setMaidenname($maidenname)
{
if (!is_string($maidenname) or !is_null($maidenname)) {
throw new InvalidArgumentException('Must be a string or null');
}
$this->maidenname = $maidenname;
}
/**
* #return string|null
*/
public function getMaidenname()
{
return $this->maidenname;
}
}
As you can see there is no way (disregarding Reflection) that you can instantiate a Person object without having the prerequisites met.
This is a good thing, because whenever you encounter a Person object, you can be a 100% sure about what kind of data you are dealing with.
Now you need a second Domain Model to handle user input, lets call it PersonForm (because it often represents a form being filled out on a website).
It has the same properties as Person, but blindly accepts any kind of data.
It will also have a list of validation rules, a method like isValid() that uses those rules to validate the data, and a method to fetch any violations.
I'll leave the definition of the class to your imagination :)
Last you need a Controller (or Service) to tie these together. Here's some pseudo-code:
class PersonController
{
/**
* #param Request $request
* #param PersonMapper $mapper
* #param ViewRenderer $view
*/
public function createAction($request, $mapper, $view)
{
if ($request->isPost()) {
$data = $request->getPostData();
$personForm = new PersonForm();
$personForm->setData($data);
if ($personForm->isValid()) {
$person = new Person(
$personForm->getFirstname(),
$personForm->getLastname(),
$personForm->getMaidenname()
);
$mapper->insert($person);
// redirect
} else {
$view->setErrors($personForm->getViolations());
$view->setData($data);
}
}
$view->render('create/add');
}
}
As you can see the PersonForm is used to intercept and validate user input. And only if that input is valid a Person is created and saved in the database.
business rules
This does mean that certain business logic will be duplicated:
In Person you'll want to enforce business rules, but it can simple throw an exception when something is off.
In PersonForm you'll have validators that apply the same rules to prevent invalid user input from reaching Person. But here those validators can be more advanced. Think off things like human error messages, breaking on the first rule, etc. You can also apply filters that change the input slightly (like lowercasing a username for example).
In other words: Person will enforce business rules on a low level, while PersonForm is more about handling user input.
more convenient
A less strict approach, but maybe more convenient:
Limit the validation done in Person to enforce required properties, and enforce the type of properties (string, int, etc). No more then that.
You can also have a list of constraints in Person. These are the business rules, but without actual validation code. So it's just a bit of configuration.
Have a Validator service that is capable of receiving data along with a list of constraints. It should be able to validate that data according to the constraints. You'll probably want a small validator class for each type of constraint. (Have a look at the Symfony 2 validator component).
PersonForm can have the Validator service injected, so it can use that service to validate the user input.
Lastly, have a PersonManager service that's responsible for any actions you want to perform on a Person (like create/update/delete, and maybe things like register/activate/etc). The PersonManager will need the PersonMapper as dependency.
When you need to create a Person, you call something like $personManager->create($userInput); That call will create a PersonForm, validate the data, create a Person (when the data is valid), and persist the Person using the PersonMapper.
The key here is this:
You could draw a circle around all these classes and call it your "Person domain" (DDD). And the interface (entry point) to that domain is the PersonManager service. Every action you want to perform on a Person must go through PersonManager.
If you stick to that in your application, you should be safe regarding to ensuring business rules :)
I think the statement "A user entering 'bad' input is not an exception: it's to be expected." is debatable...
But if you don't want to throw an exception, why don't you create an isValid(), or getValidationErrors() method?
You can then throw an exception, if someone tries to save an invalid entity to the database.
Your domain requires that when creating a person, you will provide a first name and a surname. The way I normally approach this is by validating the input model, an input model might look like;
class PersonInput
{
var $firstName;
var $surname;
public function isValid() {
return isset($this->firstName) && isset($this->surname);
}
}
This is really a guard, you can put these rules in your client side code as well to try and prevent this scenario, or you can you return from your post with an invalid person message. I don't see this as an exception, more along the lines of "to be expected" which is why I write the guard code. Your entry into your domain now might look like;
public function createPerson(PersonInput $input) {
if( $input->isValid()) {
$model->createPerson( $input->firstName, $input->surname);
return 'success';
} else {
return 'person must comtain a valid first name and surname';
}
}
This is just my opinion, and how I go about keeping my validation logic away from the domain logic.
I think your design in which the $person->surname = ''; should raise an error or exception could be simplified.
Return the error once
You dont want to catch errors all the time when assigning each value, you want a simple one-stop solution like $person->Valididate() that looks at the current values. Then when you'd call a ->Save() function, it could automatically call ->Validate() first and simply return False.
Return the error details
But returning False, or even an errorcode is often not sufficient: you want the 'who? why?' details. So lets use a class instance to contain the details, i call it ItemFieldErrors. Its passed to Save() and only looked at when Save() returns False.
public function Validate(&$itemFieldErrors = NULL, $aItem = NULL);
Try this complete ItemFieldErrors implementation. An array would suffice, but i found this more structured, versatile and self-documenting. You could always pass and parse the error details more intelligently anywhere/way you like, but often (if not always..) just outputting the asText() summary would do.
/**
* Allows a model to log absent/invalid fields for display to user.
* Can output string like "Birthdate is invalid, Surname is missing"
*
* Pass this to your Validate() model function.
*/
class ItemFieldErrors
{
const FIELDERROR_MISSING = 1;
const FIELDERROR_INVALID = 2;
protected $itemFieldErrors = array();
function __construct()
{
$this->Clear();
}
public function AddErrorMissing($fieldName)
{
$this->itemFieldErrors[] = array($fieldName, ItemFieldErrors::FIELDERROR_MISSING);
}
public function AddErrorInvalid($fieldName)
{
$this->itemFieldErrors[] = array($fieldName, ItemFieldErrors::FIELDERROR_INVALID);
}
public function ErrorCount()
{
$count = 0;
foreach ($this->itemFieldErrors as $error) {
$count++;
}
unset($error);
return $count;
}
public function Clear()
{
$this->itemFieldErrors = array();
}
/**
* Generate a human readable string to display to user.
* #return string
*/
public function AsText()
{
$s = '';
$comma = '';
foreach($this->itemFieldErrors as $error) {
switch ($error[1]) {
case ItemFieldErrors::FIELDERROR_MISSING:
$s .= $comma . sprintf(qtt("'%s' is absent"), $error[0]);
break;
case ItemFieldErrors::FIELDERROR_INVALID:
$s .= $comma . sprintf(qtt("'%s' is invalid"), $error[0]);
break;
default:
$s .= $comma . sprintf(qtt("'%s' has unforseen issue"), $error[0]);
break;
}
$comma = ', ';
}
unset($error);
return $s;
}
}
Then ofcourse there is $person->Save() that needs to receive it so it can pass it through to Validate(). In my code, whenever i 'load' data from the user (form submission) the same Validate() is called already, not only when saving.
The model, would do this:
class PersonModel extends BaseModel {
public $item = array();
public function Validate(&$itemFieldErrors = NULL, $aItem = NULL) {
// Prerequisites
if ($itemFieldErrors === NULL) { $itemFieldErrors = new ItemFieldErrors(); }
if ($aItem === NULL) { $aItem = $this->item; }
// Validate
if (trim($aItem['name'])=='') { $itemFieldErrors->AddErrorMissing('name'); }
if (trim($aItem['surname'])=='') { $itemFieldErrors->AddErrorMissing('surname'); }
if (!isValidDate($aItem['birthdate'])) { $itemFieldErrors->AddErrorInvalid('birthdate'); }
return ($itemFieldErrors->ErrorCount() == 0);
}
public function Load()..
public function Save()..
}
This simple model would hold all data in $item, so it simply exposes fields as $person->item['surname'].
I have 1 Employee entity which holds a value object of ContactInformation :
Employee.php
class Employee {
private $contactInformation;
public function __construct(ContactInformation $contactInformation) {
$this->contactInformation = $contactInformation;
}
public function getContactInformation() {
return $this->contactInformation;
}
}
ContactInformation.php
class ContactInformation {
private $email; // Required
private $cellPhone; // Required
private $phone; // Optional
private $address; // Optional
public function __construct($email, $cellPhone) {
$this->email = $email;
$this->cellphone = $cellphone;
}
/** Here goes a bunch of setter getter for those properties **/
}
If an Employee's phone is changed, is it not better to do just
$employee->getContactInformation()->setPhone($newPhone)
Rather than forcing immutability such as
$employee->setContactInformation(new ContactInformation(/** copy paste
here **/));
Going by Eric Evans' book, I understand that when you change a part of a value object, then it is a whole different value object then it was before.
I just cannot get the same conclusion about a little bit more complex object like this ContactInformation, is it supposed to be an Entity?
Need advice, thanks !
For me, Employee to have dependency on ContactInformation means that you expect to have ContactInformation already set with phones, etc. and then passed to employee, i.e.:
$contanctInfo = new ContactInformation();
$contanctInfo->setPhone(820);
$employee1 = new Employee($contanctInfo);
echo $employee1->getContanctInformation()->getPhone(); // 820
However, that means you would need to make as much ContactInformation objects, as Employee objects, before instantiating each Employee, because:
$contanctInfo = new ContactInformation();
$contanctInfo->setPhone(820);
$employee1 = new Employee($contanctInfo);
$employee2 = new Employee($contanctInfo);
$employee1->getContactInformation()->setPhone(123);
$employee2->getContactInformation()->setPhone(666);
echo $employee1->getContactInformation()->getPhone();
echo $employee2->getContactInformation()->getPhone();
Turns into:
666
666
Because you are changing one and the same object.
In your case, you are instantiating ContactInformation at the same time, when you are instantiating Employee when makes the dependency a bit useless:
$employee1 = new Employee(new ContactInformation());
$employee2 = new Employee(new ContactInformation());
$employee1->getContactInformation()->setPhone(123);
$employee2->getContactInformation()->setPhone(666);
echo $employee1->getContactInformation()->getPhone();
echo $employee2->getContactInformation()->getPhone();
Results into:
123
666
So, if you don't want to make changes on the dependent object before inject it to another object, you don't need a dependency at all. Even more, in real world ContanctInformation is bind to an Employee, not the opposite.
I would make it this way:
class Employee {
private $_contactInformation;
public function __construct() {
$this->_contactInformation = new ContactInformation($this);
}
/**
* #return ContactInformation
*/
public function getContactInformation() {
return $this->_contactInformation;
}
}
class ContactInformation {
private $_employee;
private $email; // Required
private $cellPhone; // Required
private $phone; // Optional
private $address; // Optional
public function __construct(Employee $employee) {
$this->_employee = $employee;
}
public function setPhone($phone) {
$this->phone = $phone;
}
public function getPhone() {
return $this->phone;
}
}
$employee1 = new Employee();
$employee2 = new Employee();
$employee1->getContactInformation()->setPhone(123);
$employee2->getContanctInformation()->setPhone(666);
echo $employee1->getContactInformation()->getPhone();
echo $employee2->getContactInformation()->getPhone();
On the other hand, if Employee has some identificator, so the ContanctInfo retrieves info based on the ID, you can make the injection the way you want it.
Value objects should be immutable, but ContactInformation should not be a value object.
Consider two people who share a house, and therefore have the same address and phone number (it simplifies matters if you assume they don't have a cellular phone or email).
In that situation, they'd have identical ContactInformation objects, but the contact information is still distinctly one person's or another's. If one of them moves out, or buys a cell phone then their contact information will have to change.
As you mentioned in your comment, it's better to consider ContactInformation as a mutable Entity, and not a Value Object.
Interesting discussion. What I do is to have getContactInformation return a clone.
class Employee {
public function getContactInformation() {
return clone $this->contactInformation;
}
...
$contactInformation = $employee->getContactInformation();
$contactInformation->setPhone('');
$employee->setContactInformation($contactInformation);
By doing so, the value object actually stored inside of Employee is immutable. You always pull a copy, which since it no longer linked to employee, is no longer a value object(one could argue with lots of hand waving) and thus can be modified without upsetting the DDD police.
Works for me and let's me use a form system which expects setters.
I have Yii application and two tables with same structure tbl and tbl_history:
Now want to create model so it will select table by parameter I send when calling model. For example:
MyModel::model('tbl')->find();
//and
MyModel::model('tbl_history')->find();
Find related article with solution in Yii forum. Made same changes and finally got this in MyModel:
private $tableName = 'tbl'; // <=default value
private static $_models=array();
private $_md;
public static function model($tableName = false, $className=__CLASS__)
{
if($tableName === null) $className=null; // this string will save internal CActiveRecord functionality
if(!$tableName)
return parent::model($className);
if(isset(self::$_models[$tableName.$className]))
return self::$_models[$tableName.$className];
else
{
$model=self::$_models[$tableName.$className]=new $className(null);
$model->tableName = $tableName;
$model->_md=new CActiveRecordMetaData($model);
$model->attachBehaviors($model->behaviors());
return $model;
}
}
Now when I make:
echo MyModel::model('tbl_history')->tableName(); // Output: tbl_history
It returns right value, but:
MyModel::model('tbl_history')->find();
still returns value for tbl.
Added:
public function __construct($id=null,$scenario=null){
var_dump($id);
echo '<br/>';
parent::__construct($scenario);
}
and got:
string(tbl_history)
string(tbl_history)
NULL
It means Yii makes call to model from other place but don't know from where and how to prevent it.
Also It makes 2 calls to model, is it too bad for performance?
It looks like the CActiveRecord::getMetaData() method needs to be overridden to achieve what you are looking for.
<?php
class TestActiveRecord extends CActiveRecord
{
private $tableName = 'tbl'; // <=default value
private static $_models=array();
private $_md;
public function __construct($scenario='insert', $tableName = null)
{
if($this->tableName === 'tbl' && $tableName !== null)
$this->tableName = $tableName;
parent::__construct($scenario);
}
public static function model($tableName = false, $className=__CLASS__)
{
if($tableName === null) $className=null; // this string will save internal CActiveRecord functionality
if(!$tableName)
return parent::model($className);
if(isset(self::$_models[$tableName.$className]))
return self::$_models[$tableName.$className];
else
{
$model=self::$_models[$tableName.$className]=new $className(null);
$model->tableName = $tableName;
$model->_md=new CActiveRecordMetaData($model);
$model->attachBehaviors($model->behaviors());
return $model;
}
}
public function tableName()
{
return $this->tableName;
}
/**
* Returns the meta-data for this AR
* #return CActiveRecordMetaData the meta for this AR class.
*/
public function getMetaData()
{
if($this->_md!==null)
return $this->_md;
else
return $this->_md=static::model($this->tableName())->_md;
}
public function refreshMetaData()
{
$finder=static::model($this->tableName());
$finder->_md=new CActiveRecordMetaData($finder);
if($this!==$finder)
$this->_md=$finder->_md;
}
}
Maybe it's easier to make MyModelHistory which extends MyModel and overrides only one method - tableName().
I recommend implementing single table inheritance. In order to do this you will need to combine your tables with a flag or type column that states whether or not this is a history record. I've pasted a few links at the bottom so you can see how this is implemented in Yii and listed some of the benefits below.
Benefits:
You won't need to duplicate code commonly used between the models
Changes to this table will only need to be executed once.
Changes to the parent model will only need to be made once.
Code becomes generally more maintainable and readable.
You seperate the code that belongs specifically to tbl and tbl_history
http://www.yiiframework.com/wiki/198/single-table-inheritance/
http://en.wikipedia.org/wiki/Single_Table_Inheritance
I created a solution for performing this exact action a couple of months ago. This is a completely dynamic solution, you just pass the table name like you are looking for to the model. That solution was originally designed to work with the same database structure across multiple databases, but it was trivial to adapt it to work in the same database. The documentation for that is here. I'd recommend reading over it as it has more details about CDynamicRecord
It's easy to adapt to work with multiple tables. You can download the adaptation as a gist from github.
Usage
1) Download the Gist and drop it into ext, save as CDynamicRecordSDB.php
2) Create Your model in Model, and setup up as follows:
Basically, you want to extend CDynamicRecord, and override your model() and tableName() so they are compliant with CDyanmicRecord.
<?php
Yii::import('ext.CDynamicRecordSDB');
class Test extends CDynamicRecordSDB
{
public static function model($dbConnectionString = 0, $className=__CLASS__)
{
return parent::model($dbConnectionString, $className);
}
public function tableName()
{
return $this->dbConnectionString;
}
[... Do everything else after this ...]
}
3) Setup your model as you normally would.
Usage
The usage is identical to CActiveRecord, and you can perform all actions. No surprises. Just a couple examples below.
$data = Test::model('tbl')->findAll();
$data2 = new Test('tbl');
$data2->findAll();
foreach ($data as $row)
print_r($row->attributes);
$data = Test::model('tbl_history')->findAll();
foreach ($data as $row)
print_r($row->attributes);
Limitations
The only limitation with doing this is you have to modify how relations work. IF you plan on accessing a related model (Bar), and you have no intention on calling Bar by itself. Then Bar should extend CActiveRecord, and in Foo you can define normal relations. Yii magically carries over the CDbConnectionString across the instances for you.
OTHERWISE, if you intend to access models in the same database, but also want to retain the ability to call them by themselves, then Bar should extend CDynamicModel, and Foo should have a getter defined as follows.
public function getBar()
{
return Bar::model($this->$dbConnectionString);
}
A small way but work for me for any number of table
public static $dynamic_table_name="main_table";
public static function setDynamicTable($param)
{
self::$dynamic_table_name=self::$dynamic_table_name.$param;
}
/**
* #return string the associated database table name
*/
public function tableName($param='')
{
self::setDynamicTable($param);
return self::$dynamic_table_name;
}
// to use it like
ModelName::model()->tableName('_one');
ModelName::model()->tableName('_two');
ModelName::model()->tableName('_three');
I'm working with a domain model, in which I have a Reservation class:
class Reservation
{
public function changeStatus($status) { ... }
}
Because the changeStatus() method should only be called in a context where all appropriate notifications are sent (emails, ...) I would like to restrict the call to this method to a ReservationService:
class ReservationService
{
public function confirmReservation(Reservation $reservation)
{
$reservation->changeStatus(Reservation::STATUS_CONFIRMED);
// commit changes to the db, send notifications, etc.
}
}
Because I'm working with PHP, there is no such concept as package visibility or friend classes, so my changeStatus() method is just public and therefore callable from anywhere in the application.
The only solution I found to this problem, is to use some kind of double dispatch:
class Reservation
{
public function changeStatus(ReservationService $service)
{
$status = $service->getReservationStatus($this);
$this->setStatus($status);
}
protected function setStatus($status) { ... }
}
The potential drawbacks are:
That complicates the design a bit
That makes the entity aware of the Service, no sure whether that's actually a drawback or not
Do you guys have any comment on the above solution, or a better design to suggest to restrict access to this changeStatus() method?
Use an interface which enforces the context you need:
interface INotifiable {
public function updated( $reservation );
}
class Reservation {
public function changeStatus( $status, INotifiable $notifiable ){
$this->setStatus( $status );
$notifiable->updated( $this );
}
}
class EmailNotifier implements INotifiable {
public function updated( $reservation ){
$this->sendUpdateEmail( $reservation ); //or whatever
}
}
The reservation then doesn't need to know anything about the service. An alternative would be to define events on Reservation, but that's added complexity you probably don't need.
You can send messages from one domain entity to another. Only objects that are capable of producing certain messages will call the method in question. A code snippet is below. This solution is for projects where dependency injection is a sort of religion like here PHP+DDD.
The Reservation Service gets a message factory. The factory is injected through the constructor method. Objects that don't have this factory cannot issue this sort of messages. (Of course you must restrict object instantiation to factories.)
class Domain_ReservationService
{
private $changeStatusRequestFactory;
public function __construct(
Message_Factory_ChangeStatusRequest $changeStatusRequestFactory
) {
$this->changeStatusRequestFactory = $changeStatusRequestFactory;
}
public function confirmReservation(Domain_Reservation $reservation) {
$changeStatusRequest = $changeStatusRequestFactory->make(
Reservation::STATUS_CONFIRMED
);
$reservation->changeStatus($changeStatusRequest);
// commit changes to the db, send notifications, etc.
}
}
The Reservation object checks the contents of the message an decides what to do.
class Domain_Reservation
{
public function changeStatus(
Message_Item_ChangeStatusRequest $changeStatusRequest
) {
$satus = $changeStatusRequest->getStatus();
...
}
}
Message object is a DDD value object. (Sometimes it acts like a strategy.)
class Message_Item_ChangeStatusRequest
{
private $status;
public function __construct( $status ) {
$this->$status = $status;
}
public function getStatus() {
return $this->$status;
}
}
This factory produces messages.
class Message_Factory_ChangeStatusRequest
{
public function make($status) {
return new Message_Item_ChangeStatusRequest ($status);
}
}
All domain objects are produced by this layer factory.
class Domain_Factory
{
public function makeReservationService() {
return new Domain_ReservationService(
new Message_Factory_ChangeStatusRequest()
);
}
public function makeReservation() {
return new Domain_Reservation();
}
}
The classes above can be used in your application as follows.
$factory = new Domain_Factory();
$reservationService = $factory->makeReservationService();
$reservation = $factory->makeReservation();
$reservationService->confirmReservation($reservation);
But I don't see why you don't want to use $reservation->beConfirmed() instead of passing status constants.
It actually sounds like this is missing a very important concept, namely a ProcessManager. A ProcessManager represents a distributed business transaction spanning multiple contexts. In reality it is a simple Finite State Machine.
An example workflow:
A PlaceReservationCommand is sent to the ReservationContext, which handles it and publishes a ReservationWasPlacedEvent that the ReservationProcessManager is subscribed to.
The ReservationProcessManager receives the ReservationWasPlacedEvent and validates that it can transition to the next step. I then sends a NotfiyCustomerAboutReservationCommand to the NotificationContext. It now listens to ReservationNotificationFailedEvent and ReservationNotificationSucceededEvent.
Now the ReservationProcessManager sends the ConfirmReservationCommand to the ReservationContext only when it received the ReservationNotificationSucceededEvent.
The trick here is that there is no status field in Reservation. The ProcessManager is responsible of tracking the state of this business transaction. Most likely there is a ReservationProcess Aggregate that contains the statuses like ReservationProcess::INITIATED,
ReservationProcess::CUSTOMER_NOTIFICATION_REQUESTED, ReservationProcess::CUSTOMER_NOTIFIED, ReservationProcess::CONFIRMATION_REQUESTED and ReservationProcess::CONFIRMED. The last state indicates a finite state that marks the process as done.
One of the things that the Symfony2 and FLOW3 frameworks have adopted is tagging their stable public API with an #api annotation comment.
While this is not exactly what you're looking for, it comes close. It documents the parts of your API that users can rely on. Plus your entity does not have to know about the service, avoiding the evil circular dependency.
Example:
class Request
{
/**
* Gets the Session.
*
* #return Session|null The session
*
* #api
*/
public function getSession()
{
return $this->session;
}
}