Currently we have entities that looks like this:
class MemberSession implements Entity
{
private $memberId;
private $subscriptionId;
private $username;
private $nickname;
private $trial;
private $expired;
private $email;
public function __construct(
int $memberId = null,
int $subscriptionId = null,
string $username = '',
string $nickname = '',
bool $trial = false,
bool $expired = false,
string $email = ''
) {
$this->memberId = $memberId;
$this->subscriptionId = $subscriptionId;
$this->username = $username;
$this->nickname = $nickname;
$this->trial = $trial;
$this->expired = $expired;
$this->email = $email;
}
// I some getters and setters here
public function toArray() : array
{
return [
MemberField::ID => $this->memberId,
MemberField::SUBSCRIPTION_ID => $this->subscriptionId,
MemberField::USERNAME => $this->username,
MemberField::NICKNAME => $this->nickname,
MemberField::EMAIL => $this->email,
// wait, why don't we have the value for $trial and $expired here?
];
}
And they are a pain to deal with
return (new MemberSessionBuilder)
->setMemberId($session->getMemberId())
->setSubscriptionId($session->getSubscriptionId())
->setUsername($session->getUsername())
->setNickname($request->get(MemberField::NICKNAME)) // did you notice what's going on here?
->setExpired($session->isExpired())
->setTrial($session->isTrial())
->build();
So instead of that, I propose this class
class MemberSession implements Entity
{
private $data = [
'memberId' => null,
'subscriptionId' => null,
'username' => null,
'nickname' => null,
'trial' => null,
'expired' => null,
'email' => null,
];
// I some getters and setters here
}
And the Entity class can implement these methods
class Entity
{
private $data;
public function __construct(array $rawData)
{
$camelized = $this->camelizeKeys($rawData); // use matching field names or underscored matching field names
$sanitized = array_intersect_key($camelized, $this->data); // remove fields that are not part of the entity
$this->data = array_merge($this->data, $sanitized); // update affected values
}
public function toArray() : array
{
return $this->data;
}
/**
* @param string[] $rawData
* @return string[]
* */
private function camelizeKeys(array $rawData) : array
{
$camelized = [];
foreach ($rawData as $key => $value) {
$camelized[$this->camelize($key)] = $value;
}
return $camelized;
}
private function camelize(string $word) : string
{
$camelized = str_replace('_', '', ucwords($word, '_'));
return lcfirst($camelized);
}
}
Here are a few advantages:
- All allowed fields can be used in the constructor, they are also all optional. This can efficiently replace the builder.
- All available fields are returned all the time by the
toArray
method. this is more consistant. - Entities can be copied in a single line of code
new MemberSession($otherMemberSession->toArray())
- They support matching structure from elasticsearch or mysql.
Other concerns:
Keys used in the array could be misspelled. This can be supported by adding something like this to the entity:
public function strictBuild(array $data)
{
$camelized = $this->camelizeKeys($rawData);
$sanitized = array_intersect_key($camelized, $this->data);
if (!$this->validKeys($camelized, $sanitized)) {
throw new \Exception('You are trying to fill an entity with unrecognized data');
}
$this->data = array_merge($this->data, $sanitized);
}
private function validKeys(array $camelized, array $sanitized)
{
$supporteKeys = array_keys($sanitized);
$providedKeys = array_keys($camelized);
return count(array_diff($supportedKeys, $providedKeys)) == 0);
}