-
-
Save basherr/4f5d5fef9213587120a96988162d29bb to your computer and use it in GitHub Desktop.
/// PRODUCT Data type | |
<?php | |
namespace App\DataTypes; | |
/** | |
* class Product | |
* | |
* Represent the Product type for the shopbot | |
* | |
* @package App\DataTypes | |
*/ | |
class Product extends PropertyAccessor | |
{ | |
/** | |
* @var string | |
*/ | |
protected $sku; | |
/** | |
* @var string | |
*/ | |
protected $title; | |
/** | |
* @var string | |
*/ | |
protected $image; | |
/** | |
* @var string | |
*/ | |
protected $link; | |
} | |
/// Default Property Accessor class | |
namespace App\DataTypes; | |
/** | |
* class PropertyAccessor | |
* | |
* Retrieve/changes any property of the class dynmically without the need for | |
* setters & getters | |
* | |
* To set `foo` property for a class, just call `setFoo` method on object instance | |
* where `Foo` first character will be converted to lowercase | |
* | |
* @package App\DataTypes | |
*/ | |
abstract class PropertyAccessor | |
{ | |
/** | |
* Set any property for the class dynamically | |
* | |
* @param string $name | |
* @param mixed $value | |
* @return $this | |
*/ | |
public function __call(string $name, $value) | |
{ | |
if (substr($name, 0, 3) === 'set') { | |
return $this->setProperty($name, $value); | |
} else if (substr($name, 0, 3) === 'get') { | |
return $this->getProperty($name); | |
} | |
} | |
/** | |
* Set any property for the class | |
* | |
* @param string $name | |
* @param mixed $value | |
* @return $this | |
* @throws \App\Exception\InvalidSetterException | |
*/ | |
protected function setProperty(string $name, $value) | |
{ | |
$property = lcfirst(str_replace('set', '', $name)); | |
if (property_exists($this, $property)) { | |
$this->$property = array_shift($value); | |
return $this; | |
} | |
throw new \Exception('Invalid Property Setter'); | |
} | |
/** | |
* Retrieve any property for the class dynamically | |
* | |
* @param string $name | |
* @return mixed | |
* @throws \App\Exception\InvalidGetterException | |
*/ | |
public function getProperty(string $name) | |
{ | |
$property = lcfirst(str_replace('get', '', $key)); | |
if (property_exists($this, $property)) { | |
return $this->$property; | |
} | |
throw new \Exception('Invalid Property Getter'); | |
} | |
} | |
This is fine in principle:
PropertyAccessor
should be a trait rather than a base class. Something like\App\Traits\CanAccessProperties
.Product
should beabstract
& have anabstract public function hydrate($incoming): self
.- You should probably just write a general test for this trait so you don't have to re-test it for every class.
- To allow you to easily test expected properties exist create a base/abstract
ApiModelTestCase
for the ApiModel tests to inherit from and then add a method something like:
/**
* Test we can get & set specified properties
*
* @param array $properties an array of properties that should exist
* @param object $instance the instance we are asserting against
* @param string $message optional assertion message
* @return void
*/
protected function assertPublicProperties($properties, $instance, $message = '')
{
$count = 0;
foreach ($properties as $property) {
$set = 'set' . ucfirst($property);
$get = 'get' . ucfirst($property);
$value = 'foo' . ($count++);
$instance->$set($property, $value);
$this->assertEquals($value, $instance->$get(), "$message should be able to set & get $property");
}
Now your tests can just say:
public function testPublicProperties()
{
$this->assertPublicProperties(
['sku', 'image', 'title', 'link'],
new Product()
);
}
Add the following tests for your trait testing too & make them pass:
public function testItShouldThrowExceptionWhenGettingInvalidAttribute()
{
$this->expectException(InvalidMethodException::class);
$product = (new Product())
->getFoo();
}
public function testItShouldThrowExceptionWhenSettingInvalidAttribute()
{
$this->expectException(InvalidMethodException::class);
$product = (new Product())
->setFoo();
}
@brentkelly Why Product
should be abstract? How we will instantiate an abstract class Product
? What I believe it should implement a contract to hydrate
or extend
a base abstract class that contains hydrate
method as abstract
.
Because Zest
isn't going to be the only Connector
we have long term. It should be App\ApiModels\AbstractProduct
which defines 90% of the class, but then there needs to be a connector implementation for Zest which tells it how to hydrate from a API result.
So \App\Connectors\Zest\ApiModels\Product
extends App\ApiModels\AbstractProduct
& probably has 1 method:
hydrate($productData)
- this receives a raw API product data & hydrates the properties defined inAbstractProduct
.
Then in future when we want to connect to e.g. Shopify
, we will have a \App\Connectors\Shopify\Product
which will extend AbstractProduct
, and its hydrate
method will take a Shopify product API result & translate it to hydrate the AbstractProduct
properties.
@brentkelly Perfect, thanks.
Here's the test for it.