Skip to content

Instantly share code, notes, and snippets.

@basherr
Last active June 1, 2021 00:34
Show Gist options
  • Save basherr/4f5d5fef9213587120a96988162d29bb to your computer and use it in GitHub Desktop.
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');
}
}
@basherr
Copy link
Author

basherr commented May 31, 2021

Here's the test for it.


<?php

namespace Tests\Unit\DataTypes;

use App\DataTypes\Product;
use PHPUnit\Framework\TestCase;

class ProductUnitTest extends TestCase
{
    public function testShouldSetClassAttributes()
    {
        $product = (new Product())
            ->setSku(123)
            ->setTitle('Foo')
            ->setImage('Foo-Image')
            ->setLink('Foo-Link');

        $this->assertEquals(123, $product->getSku());
        $this->assertEquals('Foo', $product->getTitle());
        $this->assertEquals('Foo-Image', $product->getImage());
        $this->assertEquals('Foo-Link', $product->getLink());
    }
}

@brentkelly
Copy link

This is fine in principle:

  • PropertyAccessor should be a trait rather than a base class. Something like \App\Traits\CanAccessProperties.
  • Product should be abstract & have an abstract 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();
}

@basherr
Copy link
Author

basherr commented Jun 1, 2021

@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.

@brentkelly
Copy link

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 in AbstractProduct.

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.

@basherr
Copy link
Author

basherr commented Jun 1, 2021

@brentkelly Perfect, thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment