Standard Data Structures

Introduction

Standard Data Structures is sets of data manipulation objects and data entities structure which provide functionalities to ease working with data sets. these libraries are used as part of core functionalities of Poirot Framework. The component are distributed under the "Poirot/Std" namespace and has a git repository, and, for the php components, a light autoloader and composer support. All Poirot components following the PSR-4 convention for namespace related to directory structure and so, can be loaded using any modern framework autoloader. 100% Test Coverage and fully testable by PHPUnit Test.

In Case You need any support or question use the links below:

Slack: getpoirot.slack.com https://join.slack.com/t/getpoirot/shared_invite/zt-dvfhkoes-h45XoKlyxamfhaHm6G4uSA

iData Interface

The iData interface provide simple interface around objects which considered as providing data properties with all objects implementing iData interface it's possible to exchange data between any data entity object.

Data Objects are: - Traversable - Countable - Implementing iObject property overloading interface.


interface iData
    extends iObject, \Countable
{
    /**
     * Set Struct Data From Array
     *
     * @param iterable $data
     *
     * @return $this
     */
    function import(iterable $data);

    /**
     * If a variable is declared
     *
     * @param mixed $key
     *
     * @return bool
     */
    function has(...$key);

    /**
     * Unset a given variable
     *
     * @param mixed $key
     *
     * @return $this
     */
    function del(...$key);

    /**
     * Clear all values
     *
     * @return $this
     */
    function empty();

    /**
     * Is Empty?
     *
     * @return bool
     */
    function isEmpty();
}

DataStruct

Implementing iData interface and is a simplest data entity object.

Constructing

The DataStructure can accept sets of data as a constructor argument. These data is an iterable object with key as data property name which hold the value of the property.

$dataClass = new DataStruct([
   'name' => 'PHP',
   'designed_by' => 'Rasmus Lerdorf',
 ]);

Constructing by variable reference

When there is an array variable, this can be referenced as a construct to the data object, make any change to the data object properties will reflect the reference variable.

$data = [ 'name' => null ];
$dataClass = DataStruct::ofReference($data);
$dataClass->name = 'Power';

print_r($data);
// ['name' => 'Power']

It's Traversable

$dataClass = new DataStruct([
    'name' => 'PHP',
    'designed_by' => 'Rasmus Lerdorf',
]);

foreach ($dataClass as $key => $val)
    echo $key . ': '. $val . PHP_EOL;

// name: PHP
// designed_by: Rasmus Lerdorf

It's Countable

count($dataClass);
$dataClass->count();

Data Manipulation, property overloading

Set Properties:

$dataClass = new DataStruct;

$dataClass->name = 'PHP';
$dataClass->designed_by = 'Rasmus Lerdorf';

Get Properties value:

echo $dataClass->name;        // PHP
echo $dataClass->designed_by; // Rasmus Lerdorf

Remove property:

unset($dataClass->name);

Check availability of property:

Note: if property exists and has null as value it also considered as available property and this method will return true.

isset($dataClass->name);

Properties are accessible by memory reference

$dataClass = new DataStruct([
  'data' => 'value'
]);

$dataRef = &$dataClass->data;
$dataRef.= '-changed';

echo $dataClass->data; 
// value-changed

Data property can be reserved by it's memory reference: the property with not assigned value will not considered as property until take a value.

$dataClass = new DataStruct;
$dataReference = &$dataClass->data;

if (!isset($dataClass->data) && 0 === count($dataClass)) {
   $dataReference = value;
}

echo $dataClass->data; 
// value

import - Set data to object

The import method will replace all existing property values by new values provided to method.

$dataClass = new DataStruct();
$dataClass->import([
   'name' => 'PHP',
   'designed_by' => 'Rasmus Lerdorf',
 ]);

del - Remove property

Any given properties to method will be removed from data object.

$dataClass->del('name');
$dataClass->del('none_exists_prop', 'designed_by');

has - Check property exists

Check whether any properties given as argument exists or not? If any of given properties exists will return true otherwise false will be returned.

Note: if property exists and has null as value it also considered as available property and this method will return true.

$dataClass->has('name'); // true
$dataClass->has('none_exists_prop', 'designed_by'); // true

empty - Remove all properties

$dataClass->empty();

isEmpty - Check has any properties

The isEmpty method will check if object has any property and return boolean value.

$dataClass->isEmpty();

DataTypedSet

The DataTypedSet is extending DataStruct and have all functionalities from that and in addition have some unique functionalities in general can be used to define custom property accessors which known as Getter/Setter methods. A getter is a method that gets the value of a specific property. On the other hand, a setter is a method that sets the value of a specific property. You don't need to use getter/setter for everything and they should used wisely only when it's needed. They might needed to be used when you have for example a property were accept only a specific PHP type. The date_time_created property can be one of those which only accept a \DateTime object.

$dataClass = new BlogPost;
$dataClass->created_date = '2020-01-14';
// Throw PropertyError Exception

$dataClass = new BlogPost;
$dataClass->created_date = new \DateTime('2020-01-14');

Read-only properties

Read-only properties are created by defining only a getter method for a property. A PropertyIsGetterOnlyError exception is thrown in attempt to set a read-only property.

Methods with protected visibility which are prefixed with get, has and is all determined as the property getter. like: getPropertyName(), hasAnyOrder(), isAdministration(). Note: adding docblock for property is not mandatory but it's recommended.

/**
 * @property-read $property
 */
class ReadOnlyProperty
    extends DataTypedSet
{
    protected function getProperty()
    {
        return 'value';
    }
}

$a = new ReadOnlyProperty;
echo $a->property;     // value
$a->property = null;   // throws PropertyIsGetterOnlyError

Protect construct level properties by Read-only

Read-only properties are often used to provide read access to a property that was provided during construct, which should stay unchanged during the life time of an instance.

In this example below, we can have access to the connection used by Model class afterward and options is used by class itself for some internal usage inside the class and it's not a property accessor.

/**
 * @property-read Connection $connection
 */
class Model
    extends DataTypedSet
{
    /** @var Connection */
    private $connection;
    
    protected $options;

    function __construct(Connection $connection, array $options = [])
    {
        $this->connection = $connection;
        $this->options = $options;
    }
    
    // Getter method
    
    protected function getConnection()
    {
        return $this->connection;
    }
}

$model = new Model(new Connection);

$model->connection = null; // throws PropertyIsGetterOnlyError
unset($model->connection); // throws PropertyIsGetterOnlyError

Write-only properties

Read-only properties are created by defining only a setter method for a property. A PropertyIsSetterOnlyError exception is thrown in attempt to get a write-only property.

Methods with protected visibility which are prefixed with set, give all determined as the property getter. like: setPropertyName($name), giveConnection($connection). Note: adding docblock for property is not mandatory but it's recommended.

/**
 * @property-write $property
 */
class WriteOnlyProperty
    extends DataTypedSet
{
    private $property;
    
    protected function setProperty($value)
    {
        return $this->property = $value;
    }
}

$a = new WriteOnlyProperty;
$a->property = 'value';
echo $a->property;           // throws PropertyIsSetterOnlyError

Property Type declaration

Type declarations allow property to require a certain type at setter/getter level but mostly it would define on setters. On set property value the PropertyError will thrown if value type is mismatch by method arguments definition. When type declaration not happen on method argument and check happen inside the method block for given type and it's not match the PropertyTypeError expected to thrown as well.

When using Type Declaration the method argument should be nullable. it's needed when we gonna to unset or remove property by unseting property the underlying value will set to null.

/**
 * @property-write $created_date
 */
class BlogPost
    extends DataTypedSet
{
    protected $date_created;
    protected $tags;
    
    protected function setCreatedDate(?\DateTime $datetime)
    {
        return $this->date_created = $datetime;
    }
    
    protected function setTags($tags)
    {
       if (!is_array($tags) || empty($tags))
           throw new Struct\PropertyTypeError(
              'tags should be none empty array.'
           );
    }
    
    // ...
}

import - Set data to object

The import method will replace all existing property values by new values provided to method.

As DataTypedSet could be sensitive to data given to properties couple of exceptions can happen when importing data.

Here is an example how the exceptions can be ignored while importing data:

$DataTypedSet = new ErrorProneDataTypedSet;

// Ignore All Errors Related To Getter Only Property
$DataTypedSet->import($someData, [PropertyIsGetterOnlyError::class]);

// Ignore All Errors Related To Getter Only Property and Immutable Property
$DataTypedSet->import($someData, [
   PropertyIsGetterOnlyError::class, 
   PropertyIsImmutableError::class
]);

// Ignore All Property Error, including getter only, immutable, etc.
$DataTypedSet->import($someData, [PropertyError::class]);

// Ignore All Exceptions regarding if it's property error or anything
$DataTypedSet->import($someData, [\Exception::class]);

more methods ...

The DataTypedSet is extending DataStruct and they have same functionalities except methods mentioned above hereto know more about available methods check the documentation from DataStruct .

DataHashMap

The DataTypedSet is extending DataStruct and have all similar functionalities from that. what specific is about hash map data object is allow to set none scalar keys for data.

$dataClass = new DataHashMap;
$dataClass->set([1, 2, 3], 6);

echo $dataClass->get([1, 2, 3]); // 6

As none scalar types can be assigned as a key holding values there is no possibility to use property accessors, so two methods are added to DataHashMap object:

set - Set property

$dataClass = new DataHashMap;

$dataClass->set('name', 'PHP');

$closure = function () {};
$dataClass->set($closure, 'value');

get - Get property

$dataClass->get('name');
$dataClass->get($closure);
$dataClass->get([1, 2, 3]);

del - Remove property

$dataClass->del('name');
$dataClass->del($closure);
$dataClass->del([1, 2, 3]);

has - Check property exists

$dataClass->has('name');
$dataClass->has($closure);
$dataClass->has([1, 2, 3]);

more methods ...

The DataTypedSet is extending DataStruct and have all functionalities from that to know more about available methods check the documentation from DataStruct .

CollectionObject

The CollectionObject is using to store any kind of data type in a collection set, it's possible to define set of meta data to it when data is stored, which can used to query the whole data based on theses associated meta data. for every same data a unique datum id will be generated which is used as a reference to fetch data again.

It's Countable

// Add random data amount
for ($i=1; $i<=rand(1, 10); $i++) {
   $collectionObject->add(uniqid());
}

$collectionObject->count();
count($collectionObject);

ObjectID for data type

Every object when stored to the collection will get a unique ID and can be used to access to the object. here is how the uniqueID can be generated in forehand for an object.

$objectToStore = [1, 2, 3, 4, 5];
$objectId = DatumId::of($objectToStore);
echo $objectId->__toString();
// 00d34736902d23bd0da1e4de9e79d94a


$objectToStore = pack("nvc*", 0x1234, 0x5678, 65, 66);
$objectId = DatumId::of($objectToStore);
echo $objectId->__toString();
// 3f5145ad7bb6fe230a3badefc32f90e7

add - Adding data

Add data to the collection, data can be any value type. with the second argument it's possible to define set of meta data to the stored data which can fetched later or used as a term when querying the collection.

Note: when same Data is exists in collection the meta data will be replaced with the given meta data in add method.

$collectionObject = new CollectionObject;

$objectId = $collectionObject->add([1, 2, 3, 4, 5]);
$objectId = $collectionObject->add('abcdef');


// Associated meta data to stored object
//
$objectId = $collectionObject->add(new User(1), [
  'username' => 'mail@email.com'
]);

$objectId = $collectionObject->add(
   pack("nvc*", 0x1234, 0x5678, 65, 66),
   [
     'pack_format' => 'nvc*',
   ]
);

get - Get stored object by objectId

Get stored object data in collection by its unique object id, will return null if not found.

$object = $collectionObject->get(
  new DatumId('00d34736902d23bd0da1e4de9e79d94a')
);

Get additional meta data associated with the object: with a second argument the meta data and object itself is available to the callback function. if object with given id not exists in collection null will returned without calling the given callback function.

$objectId = $collectionObject->add(new User(1), [
  'username' => 'mail@email.com'
]);

[$object, $objData] = $collectionObject->get($objectId,
   function($obj, $meta) {
      return [$obj, $meta];
   }
);

print_r($objectData);
// [ 'username' => 'mail@email.com' ]

find - Find an object by meta data

Search for stored objects that match given accurate meta data. The result returned is the IteratorWrapper instance include all the object matched with the criteria data.

Fetch all objects inside collection:

foreach ($collectionObject->find([]) as $hashIdAsString => $object) {
    // Do anything with all stored objects
}

Find based on the criteria matching the metadata associated with each object:

$data = ['name' => 'PHP', 'designed_by' => 'Rasmus Lerdorf' ];
$collectionObject->add((object) $data, $data);

$object = $collectionObject->find(['name' => 'PHP'])->current();
/*
object(stdClass)
  public 'name' => string 'PHP'
  public 'designed_by' => string 'Rasmus Lerdorf'
*/

del - Remove object from collection

Remove an object by the ObjectID. if no object with same ObjectID exists on collection it will do nothing.

$collectionObject->del(
   DatumId::of([1, 2, 3, 4, 5])
);

setMetaData - Set object meta data

Set meta data associated with the existence object. It will thrown an Exception if object not found.

$objectId = $collectionObject->add(new User(1));
$collectionObject->setMetaData($objectId, ['meta' => 'data']);

$objData = $collectionObject->get($objectId, function($_, $meta) {
    return $meta;
});

print_r($objData);
// ['meta' => 'data']

CollectionPriorityObject

The CollectionPriorityObject is extending CollectionObject to allow add objects with priority order. To read about all available methods see the CollectionObject.

add - Adding data

The third argument of add method indicate the priority order of the object.

$collectionObject = new CollectionPriorityObject;
$collectionObject->add('C', [], 2);
$collectionObject->add('A', [], 4);
$collectionObject->add('D', [], 1);
$collectionObject->add('B', [], 3);

foreach($collectionObject->find([]) as $storedObj) {
    echo $storedObj;
}

// ABCD

priority order also can be defined as the meta data:

$collectionObject->add('C', ['__order' => '2']);
$collectionObject->add('A', ['__order' => 4]);
$collectionObject->add('D', ['__order' => 1]);
$collectionObject->add('B', ['__order' => 3]);

more methods ...

More methods is similar with CollectionObject, To read about all available methods see the CollectionObject documentation.

Value Object Abstraction

In practical terms you can think of value objects as your own primitive types. And they should behave as ones. It means that value objects should always be immutable! This is crucial otherwise it’s hard to avoid issues with values changing unexpectedly (values should be replaced, not modified!).

It’s important to remember about immutability (none of these methods can modify internal state) and to not to put any actual business logic inside value objects – they should be as self-contained as possible.

Declaration

To define a value object of any type it should extends the aValueObject abstract class, and you should follow these simple rules when working with ValueObject in Poirot:

Constructor:

  • Value Object will have __construct method.

  • Constructor method should have all possible Value Object needed properties as arguments in constructor method.

  • Each constructor arguments should have an accessor method with same name of argument. for example, if you have $amount variable it should have one protected accessor method named withAmount($value) which can change the state of object for the property.

  • The parent __construct method call on constructing object to initialize the init state.

class Money
  extends aValueObject
{
    private $amount;
    protected $currency;

    function __construct(int $amount, string $currency = 'EUR')
    {
        parent::__construct();

        $this->withAmount($amount);
        $this->withCurrency($currency);
    }

    protected function withAmount($amount)
    {
        $this->amount = $amount;
        return $this;
    }

    protected function withCurrency($currency)
    {
        $this->currency = $currency;
        return $this;
    }
    
    // Rest of implementation ...
}

Validation:

When creating a new value object we need to make sure that it has a valid state on constructing. when values are not correct Exception should thrown on creating object. To read more see Validation section.

Getting current state properties:

To know what state Value Object is and to get property values that we need to be expose to outside the getter methods can be defined:

class Money
  extends aValueObject
{
    private $amount;
    protected $currency;

    function __construct(int $amount, string $currency = 'EUR')
    {
        parent::__construct();

        $this->withAmount($amount);
        $this->withCurrency($currency);
    }

    protected function withAmount($amount)
    {
        $this->amount = $amount;
        return $this;
    }

    protected function withCurrency($currency)
    {
        $this->currency = $currency;
        return $this;
    }
    
    // Getter Methods:
    
    protected function getAmount()
    {
        return $this->amount;
    }
    
    protected function getCurrency()
    {
        return $this->currency;
    }
}

$moneySpent = new Money(100, 'USD');
echo $moneySpent->getAmount() . ' ' . $moneySpent->getCurrency();
// 100 USD

Change object state and immutability:

Changing the object state and immutability will handled by abstract class the only thing is to define an accessor protected method that is prefixed by with. Check the example below and how it's been used:

$eur = new Money(100, 'EUR');
$usd = $eur->withCurrency('USD'); // change the state and return new object

echo $eur->getCurrency(); // EUR
echo $usd->getCurrency(); // USD

to make it understandable to most IDEs some notations can be added to class:

/**
 * @method Money  withAmount($amount)
 * @method Money  withCurrency($currency)
 *
 * @method int    getAmount()
 * @method string getCurrency()
 */
class Money
  extends aValueObject
{
    private $amount;
    protected $currency;

    function __construct(int $amount, string $currency = 'EUR')
    {
        parent::__construct();

        $this->withAmount($amount);
        $this->withCurrency($currency);
    }

    protected function withAmount($amount)
    {
        $this->amount = $amount;
        
        // This is not mandatory to have but can help with auto-completion 
        // in code editors
        return $this;
    }
    
    // Rest of implementation here
    // ...

It's Traversable

$money = new Money(100, 'EUR');

$asArray = iterator_to_array($money);
/*
['amount' => 100, 'currency' => 'EUR'];
*/

It's Serializable

$money = new Money(100, 'EUR');
$serialized = serialize($money);

$moneyUnserialized = unserialize($serialized);
echo $moneyUnserialized->getAmount(); 
// 100

Check equality

Two value objects are equal not when they refer to the same object, but when the value that they hold is equal, e.g. two different one hundred dollar bills have the same value when we use them to buy something (class Money would be a value object). To check equality of two value object the abstract object will check the whole values one by one and the class implementation to see if the given object is extend the current value object, which mean something similar to this method:

function isEqualTo(iValueObject $object): bool
{
   return get_class($this) === get_class($object) && $this == $object;
}

The isEqualTo method can be override by object extending abstract value object, for the Money object it could be re written:

class Money
  extends aValueObject
{
    private $amount;
    protected $currency;

    function __construct(int $amount, string $currency = 'EUR')
    {
        parent::__construct();

        $this->withAmount($amount);
        $this->withCurrency($currency);
    }
    
    function isEqualTo(iValueObject $object): bool
    {
        if (! $object instanceof Money) 
           return false;
           
        return $this->getAmount() === $object->getAmount()
            && $this->getCurrency() === $object->getCurrency();
    }
    
    // Rest of implementation here
    // ...
}


$eur = new Money(100);
$usd = $eur->withCurrency('USD');

var_dump($eur->isEqualTo($usd)); // false

Validation

Let’s take currency from our Money object, Currency value should always consist of three uppercase letters, so why not to convert it to its own Value Object:

Validation should happen on construct level and when value object state got changed by property accessor as well.

Here we only make sure that currency code has a correct format, but as an improvement it could also validated against ISO 4217 currency list (or its subset).

class Currency
  extends aValueObject 
{
    private $currency;
 
    function __construct(string $currency)
    {
        // The exception will thrown if currency is invalid.
        $this->withCurrency($currency);
    }
 
    function withCurrency(string $currency)
    {
       $currency = strtoupper($currency);
        if (strlen($currency) !== 3 || !ctype_alpha($currency))
            throw new InvalidArgumentException(
              'Currency has to consist of three letters'
            );
            
        $this->currency = $currency;
        return $this;
    }
    
    // Rest of the definition should be here
    // ...
}

Additional helper methods

In addition to standard getter methods for internal state retrieval, VOs can have helper methods with various functionality. This includes creating new values (either entirely new or based on the current one, e.g. result of adding some amount to Money object), providing different representation for its internal state (e.g. IPAddress VO can present IP address as string or binary data), checking if value object meets some condition (e.g. DateRange VO can have a method to find out if it overlaps another DateRange object), etc.

class Money
  extends aValueObject
{
    // Money definition from previous example should be here
 
    function add(Money $toAdd): Money
    {
        if ($this->getCurrency() !== $toAdd->getCurrency())
            throw new InvalidArgumentException(
                'You can only add money with the same currency'
            );
        
        if ($toAdd->getAmount() === 0)
            return $this;
        
        return new Money(
           $this->getAmount() + $toAdd->getAmount(), 
           $this->getCurrency()
        );
    }
}

PriorityQueue

A PriorityQueue is very similar to a Queue. Values are pushed into the queue with an assigned priority, and the value with the highest priority will always be at the front of the queue. The PHP Default SplPriorityQueue acts as a heap; on iteration, each item is removed from the queue. This class will allow to iterate multiple time over internal queue which is implemented default php SplPriorityQueue.

Iterating over a Queue and successive pop operations will not remove items from the queue.

Simple Usage

The PriorityQueue only has a method insert and provide Traversable object to travers over the items in queue without deleting them.

$queue = new PriorityQueue(PriorityQueue::Ascending);
$queue->insert('C', 3);
$queue->insert('D', 4);
$queue->insert('B', 2);
$queue->insert('A', 1);

foreach ($queue as $value) {
    echo $value;
}

// ABCD

SplQueue

There is some workaround and fixes for default PHP SplPriorityQueue which is available as these classes. these two classes are extend SplPriorityQueue and used internally by PriorityQueue.

SplDescendingQueue

SplAscendingQueue

Last updated

Was this helpful?