Skip to content

Instantly share code, notes, and snippets.

@angorb
Last active June 9, 2021 21:58
Show Gist options
  • Save angorb/f48a254410de513d3737813ba245b9ba to your computer and use it in GitHub Desktop.
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
<?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