Last active
June 9, 2021 21:58
-
-
Save angorb/f48a254410de513d3737813ba245b9ba to your computer and use it in GitHub Desktop.
[Dehydrated PHP Object Trait] Creates a 'hydratable' object that can be cleanly and easily converted into an associative array or JSON object for use when rapidly modeling API request payloads. #php #object #model #hydration
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Usage Example: | |
* ---------------------------------------------------------- | |
* class Ramen | |
* { | |
* use Hydratable; | |
* | |
* protected $properties = [ | |
* 'name' => [ | |
* 'required' => true, | |
* 'type' => 'string' | |
* ], | |
* 'isSpicy' => [ | |
* 'required' => false, | |
* 'type' => 'boolean' | |
* ], | |
* 'isSalty' => [ | |
* 'required' => true, | |
* 'type' => 'boolean' | |
* ], | |
* 'servingSizeInOunces' => [ | |
* 'required' => true, | |
* 'type' => 'number' | |
* ], | |
* 'container' => [ | |
* 'required' => false, | |
* 'type' => 'Vendor\\Namespace\\SpecificBowlObject' | |
* ], | |
* ]; | |
* | |
* public static $servingSizes = [8, 16, 32, 64]; | |
* | |
* public function __construct(array $properties) | |
* { | |
* $this->loadProperties($properties); | |
* } | |
* | |
* public function setIsSalty(boolean $salty) | |
* { | |
* if(!$salty){ | |
* throw new \Exception("Liar!"); | |
* } | |
* | |
* $this->isSalty = $salty; | |
* } | |
* | |
* public function validate(){ | |
* $this->checkEnum('servingSizeInOunces', self::$servingSizes); | |
* parent::validate(); | |
* } | |
* } | |
* | |
* $mySnack = new Ramen([ | |
* 'name' => 'Stanky Noodles', | |
* 'isSpicy' => true | |
* ]); | |
* | |
* $mySnack->setIsSalty(true); | |
* | |
* $mySnack->servingSizeInOunces = 16; | |
* | |
* $mySnackIsValid = $mySnack->validate(); | |
* | |
* */ | |
trait Hydratable | |
{ | |
// for use in object constructors | |
private function loadProperties(array $properties) | |
{ | |
foreach ($properties as $name => $value) { | |
if (\array_key_exists($name, $this->properties)) { | |
$this->$name = $value; | |
} | |
} | |
} | |
public function __set($name, $value) | |
{ | |
// prefer explicit 'setter' method, if one exists | |
$methodName = "set" . \ucfirst($name); | |
if (\method_exists($this, $methodName)) { | |
$this->$methodName($value); | |
return true; | |
} | |
// otherwise, assign the value to the property directly | |
// if that property is defined in the specific object model | |
if (\array_key_exists($name, $this->properties)) { | |
$this->$name = $value; | |
return true; | |
} | |
// can't set an undefined property | |
return false; | |
} | |
public function __get($name) | |
{ | |
// prefer explicit 'getter' method, if one exists | |
$methodName = "get" . \ucfirst($name); | |
if (\method_exists($this, $methodName)) { | |
return $this->$methodName(); | |
} | |
// otherwise, assign the value to the property directly | |
// if that property is defined in the specific object model | |
if (\array_key_exists($name, $this->properties) && isset($this->$name)) { | |
return $this->$name; | |
} | |
// can't get an undefined property | |
return null; | |
} | |
/** | |
* Checks if all required propertes of the object have been set | |
* | |
* @return mixed | |
*/ | |
public function isComplete() | |
{ | |
$requiredProperties = $setProperties = 0; | |
foreach ($this->properties as $propertyName => $propertyInfo) { | |
if ($propertyInfo['required']) { | |
$requiredProperties++; | |
if (isset($this->$propertyName)) { | |
$setProperties++; | |
} | |
} | |
} | |
return $requiredProperties === $setProperties; | |
} | |
public function validate() | |
{ | |
if (!$this->isComplete()) { | |
return false; | |
} | |
foreach ($this->properties as $propertyName => $propertyInfo) { | |
if (isset($this->$propertyName)) { | |
// check objects based on class name | |
if (\gettype($this->$propertyName) == 'object') { | |
if (\get_class($this->$propertyName) == $propertyInfo['type']) { | |
continue; | |
} | |
} | |
// handle generic "number" properties | |
if ($propertyInfo['type'] === 'number' && \is_numeric($this->$propertyName)) { | |
continue; | |
} | |
// handle dates & datetimes | |
if ($propertyInfo['type'] === 'date' || $propertyInfo['type'] === 'datetime') { | |
$this->$propertyName = \strtotime($this->$propertyName); | |
} | |
// check object against type primitives (integer, string, object, etc.) | |
if (\gettype($this->$propertyName) !== $propertyInfo['type']) { | |
throw new \UnexpectedValueException( | |
"Wrong datatype for property {$this->$propertyName} | |
(expected {$propertyInfo['type']}, got " . \gettype($this->$propertyName) . ")" | |
); | |
} | |
} | |
} | |
// ran through the properties loop and all looks well | |
return true; | |
} | |
protected function checkEnum($property, array $enumValues) | |
{ | |
// handle enumerated strings for value of 'packageType' property | |
if (isset($property) && !\in_array($property, $enumValues)) { | |
throw new \UnexpectedValueException( | |
"{$property} is not a valid enumerated value" | |
); | |
} | |
} | |
public function toArray() | |
{ | |
$vars = \get_object_vars($this); | |
unset($vars['properties']); | |
// Use the defined forArray method for any object that implements it. | |
// This provides a way to force class-specific validation rules when | |
// nesting Hydratable objects, especially when using them as JSON payloads | |
\array_walk_recursive($vars, function (&$var) { | |
if (\is_object($var) && \method_exists($var, 'toArray')) { | |
$var = $var->toArray(); | |
} | |
}); | |
return $vars; | |
} | |
public function json(int $flags = 0, int $depth = 512) | |
{ | |
return \json_encode($this->toArray(), $flags, $depth); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment