Purpose:
I want to accept the value of a checkbox or database field and make a Value Object out of it. The input could be any of y, Y, n, N, Yes, No, yes, no, 1, 0, true, false, or NULL.
In the case of NULL (from the DB), I want to be able to distinguish between 'no' and 'no choice has been made', so __toString()
reports it as an empty string.
<?php declare(strict_types=1);
// namespace ValueObject; // implementing later
/**
* Boolean Value Object
*
* Accepts common descriptions of boolean value.
* NULL is a special case:
*
* if allowed (via constructor),
* `isTrue()` => FALSE
* `equals(NULL) => TRUE
* `__toString` => ''
*
* but by default,
* NULL throws InvalidArgumentException.
*
* Allows a different formatting for saving to database.
*/
class Boolean
{
const TRUTHY = ['T','TRUE','Y','YES','1'];
const FALSEY = ['F','FALSE','N','NO','0'];
const DB_TRUE = 'Y';
const DB_FALSE = 'N';
const HUMAN_READABLE_TRUE = 'Yes';
const HUMAN_READABLE_FALSE = 'No';
const HUMAN_READABLE_NULL = '';
private ?bool $val;
public function __construct($bool, ?bool $allowNull = FALSE)
{
$this->val = NULL;
$this->assignifBoolean($bool);
$this->assignIfTruthy((string)$bool);
$this->assignIfFalsey((string)$bool);
if($bool && is_null($this->val)) {
throw new \InvalidArgumentException('Not a recognized boolean value');
}
if(!$allowNull && is_null($bool)) {
throw new \InvalidArgumentException('Null value for boolean not allowed');
}
}
private function assignifBoolean($bool) : void
{
if(is_bool($bool)) {
$this->val = $bool;
}
}
private function assignIfTruthy(string $bool) : void
{
if(in_array(strtoupper($bool), self::TRUTHY)) {
$this->val = TRUE;
}
}
/**
* Assign FALSE only if in the list of falsey values.
*
* This does NOT assign FALSE TO NULL!
*
* @param string $bool [description]
* @return [type] [description]
*/
private function assignIfFalsey(string $bool) : void
{
if(in_array(strtoupper($bool), self::FALSEY)) {
$this->val = FALSE;
}
}
public function equals($val) : bool
{
return $val === $this->val;
}
public function isTrue(): bool
{
return $this->val === TRUE;
}
public function __toString() : string
{
if(!is_null($this->val)) {
return $this->val ? self::HUMAN_READABLE_TRUE : self::HUMAN_READABLE_FALSE;
}
return self::HUMAN_READABLE_NULL;
}
public function toDB() : ?string
{
if(!is_null($this->val)) {
return $this->val ? self::DB_TRUE : self::DB_FALSE;
}
return NULL;
}
}
// SAMPLE TESTING
$testValues = [
'y' => TRUE,
'Y' => TRUE,
'n' => FALSE,
'N' => FALSE,
'Yes' => TRUE,
'No' => FALSE,
'True' => TRUE,
'False' => FALSE,
'T' => TRUE,
'F' => FALSE,
1 => TRUE,
'1' => TRUE,
0 => FALSE,
'0' => FALSE,
'NULL' => NULL
];
foreach($testValues as $k => $v) {
try {
$name = $k;
if($k == 'NULL') { $k = NULL; }
$tf = new Boolean($k, TRUE);
// $tf = new Boolean($k);
print "$name ($tf) ";
print $tf->isTrue() ? ' is true ' : ' is false ';
if(!$tf->equals($v)) {
throw new Exception("$name generated Wrong Value\n");
}
print "\n";
} catch(Exception $e) {
print $e->getMessage();
}
}
Questions
- Does this object know too much (i.e., does
toDB()
violate SRP)? - Formatting output in the Value Object makes sense to me; it seems like something the object would know about itself. But is it common practice?
- The method
isTrue()
is just a convenient way to writeequals(TRUE)
. Is it bad practice?
Any other comments/critique welcome
$allowNull
allowed to be passed in asnull
? Shouldn't those exceptions be thrown at the start of the constructor? Why are theassignifTruthy()
andassignifFalsey()
methods unconditionally called even ifassignifBoolean()
deemed the input as a boolean? \$\endgroup\$null
(a lack of decision/value)? \$\endgroup\$?bool $allowNull = FALSE
. That's a bug just waiting to happen. Also appreciate the suggestion of adding'null'
,'NULL'
, and''
. \$\endgroup\$