Skip to content
comhon-project edited this page Dec 3, 2020 · 58 revisions

Table of contents

  1. Preamble
  2. Root model
  3. Share id
  4. Object Collection
    1. Inheritance
  5. Main Object Collection
  6. Object validation
  7. Object serialization
    1. Serialization
      1. Operations
        1. Create
        2. Update
        3. Patch
    2. Deserialization
      1. Via model
      2. Via object
      3. Via object value
        1. Unique value
        2. Aggregation value
    3. Database time zone
  8. Object interfacing
    1. Interfacer
    2. Export
      1. Foreign properties export
      2. Inheritance
        1. Object properties
        2. Foreign properties
        3. Object root
    3. Import
      1. Foreign properties import
    4. Array Interfacing
    5. Interfacing preferences
      1. privateContext
      2. serialContext
      3. dateTimeFormat
      4. dateTimeZone
      5. updatedValueOnly
      6. propertiesFilters
      7. flattenValues
      8. stringifiedValues
      9. flagValuesAsUpdated
      10. flagObjectAsLoaded
      11. validate
      12. mergeType
      13. verifyReferences
    6. Duplicated objects
    7. Isolated value
    8. Exception management
  9. Object cast
    1. Example
    2. Limitation
    3. Automatic cast

Preamble

For the most of following examples we will use Test\Person model defined in manifest person example. We will use Test\Person\Man and Test\Person\Woman too that inherit from Test\Person. We suppose that these three models have the same serialization.

Root model

The root model Comhon\Root is a specific model that doesn't have properties. If you define a manifest without extends model(s), it will extends Comhon\Root automatically. It permit to have a unique parent model for all models. And you can interface easily any object from this model.

Share id

The share id permit to share identifiers between several models. if models share identifiers, comhon objects can't have same identifiers. For example if we have Test\Person\Woman and Test\Person\Man models and they are serialized in same SQL table person, they can't have same identifiers. On the contrary if they are NOT serialized in same SQL table, they may have same identifiers.
Actually when models share same serialization, they automatically share ids (you don't have to define it).
Only inherited models may share ids, so to share ids between Test\Person\Woman and Test\Person\Man these models must actually inherit from same model Test\Person.
The share id is used in object collection storage and during object interfacing.

The share id must be defined in manifest (see share id chapter).

Object Collection

An Object Collection permit to store objects and find them quickly and easily. Objects are indexed by model and by id. Obviously an object without id property cannot be stored in Object Collection.

Example of an Object Collection structure :

[
    Test\Person : [
        an_id_one : person_object_one,
        an_id_two : person_object_two
    ],
    Test\Town : [
        an_id_one : town_object_one
    ],
    Test\House : [
        an_id_one : house_object_one,
        an_id_two : house_object_two
    ]
]

Example of usage :

$person = new ComhonObject('Test\Person');
$person->setId(1);

$objectCollection = new OjectCollection();
$objectCollection->addObject($person);

$objectCollection->hasObject(1, 'Test\Person');           // return true
$object = $objectCollection->getObject(1, 'Test\Person'); // return object previously added

$objectCollection->hasObject(2, 'Test\Person');           // return false
$object = $objectCollection->getObject(2, 'Test\Person'); // return null

$objectCollection->hasObject(1, 'Test\House');            // return false
$object = $objectCollection->getObject(1, 'Test\House');  // return null

Be careful if you update your object id, it will not be reindexed (you have to do it yourself if necessary).

$person = new ComhonObject('Test\Person');
$person->setId(1);

$objectCollection = new OjectCollection();
$objectCollection->addObject($person);

$person->setId(2);

$objectCollection->hasObject(1, 'Test\Person');           // return true
$object = $objectCollection->getObject(1, 'Test\Person'); // return object previously added

$objectCollection->hasObject(2, 'Test\Person');           // return false
$object = $objectCollection->getObject(2, 'Test\Person'); // return null

Inheritance

To explain inheritance with share id in Object Collection let's start with an example. We remind that, Test\Person\Man that inherit from Test\Person and these models have the same serialization (so they share identifiers).
If an Object with model Test\Person\Man is added in Object Collection, you can access it by using model name Test\Person.

$man = new ComhonObject('Test\Person\Man');
$man->setId(1);

$objectCollection = new OjectCollection();
$objectCollection->addObject($man);

$objectCollection->hasObject(1, 'Test\Person'); // return true
$object = $objectCollection->getObject(1, 'Test\Person'); // return object previously added

It works in the other way

$person = new ComhonObject('Test\Person');
$person->setId(2);

$objectCollection = new OjectCollection();
$objectCollection->addObject($person);

$objectCollection->hasObject(2, 'Test\Person\Man'); // return true
$object = $objectCollection->getObject(2, 'Test\Person\Man'); // return object previously added

Note that it is possible only if models have the same serialization.

Main Object Collection

MainObjectCollection is a singleton that extends ObjectCollection. It store automatically objects with main models having id property(ies). This singleton is useful to know which object already exists and avoid redundant deserializations.

Important !!! Only objects, with models that have been described by manifests with is_main attribute or with attached serialization, are stored in singleton MainObjectCollection (see Manifest pages).

To retrieve an object call :

$object = MainObjectCollection::getInstance()->getObject('an_id', 'a_model_name'); // null if object not found

Objects are automatically added when :

$model = ModelManager::getInstance()->getInstanceModel('Test\Person');

$person1 = MainObjectCollection::getInstance()->getObject(1, 'Test\Person'); // null
$person2 = $model->loadObject(1);
$person3 = MainObjectCollection::getInstance()->getObject(1, 'Test\Person');

echo $person2 === $person3 ? 'same instance' : 'different instance';
// output 'same instance'
$model = ModelManager::getInstance()->getInstanceModel('Test\Person');
$stdInterfacer = new StdObjectInterfacer();
$stdOject = json_decode('{"id":1,"firstName":"John"}');

$person1 = MainObjectCollection::getInstance()->getObject(1, 'Test\Person'); // null
$person2 = $stdInterfacer->import($stdOject, $model);
$person3 = MainObjectCollection::getInstance()->getObject(1, 'Test\Person');

echo $person2 === $person3 ? 'same instance' : 'different instance';
// output 'same instance'
  • when you set id of an object
$person1 = MainObjectCollection::getInstance()->getObject(1, 'Test\Person'); // null
$person2 = new ComhonObject('Test\Person');
$person2->setValue('id', 1);
$person3 = MainObjectCollection::getInstance()->getObject(1, 'Test\Person');

echo $person2 === $person3 ? 'same instance' : 'different instance';
// output 'same instance'

Contrary to an instance of ObjectCollection that you have instanciated yourself, Comhon! is able to reindex objects in MainObjectCollection if their id have been updated.

$person = new ComhonObject('Test\Person');
$person->setValue('id', 1);
$person2 = MainObjectCollection::getInstance()->getObject(1, 'Test\Person'); // return same instance as $person

$person->setValue('id', 2);
$person2 = MainObjectCollection::getInstance()->getObject(1, 'Test\Person'); // return null
$person2 = MainObjectCollection::getInstance()->getObject(2, 'Test\Person'); // return same instance as $person

Note that if an instance is already added in MainObjectCollection it will not be replaced.

$person = new ComhonObject('Test\Person');
$person->setValue('id', 1);

$person2 = new ComhonObject('Test\Person');
$person2->setValue('id', 1);

$person3 = MainObjectCollection::getInstance()->getObject(1, 'Test\Person');

echo $person === $person3 ? 'same instance' : 'different instance';
// output 'same instance'

echo $person2 === $person3 ? 'same instance' : 'different instance';
// output 'different instance'

Object validation

Comhon objects, Comhon arrays and Comhon properties may be validated.

  • Comhon object validation look at required values, depencencies and conflicts
  • Property value validation look at model value and verify if restrictions are satisfied (not null, regex...)
  • Comhon array validation look at array size
  • Comhon array element validation look at model value and verify if restrictions are satisfied (not null, regex...)

Some validations are done automatically, and if validation is not satisfied, an exception is thrown. For example :

  • When a value is set on a Comhon object it is automatically validated
  • A Comhon object is automatically validated before serialization (except for patch operation)
  • Everything is validated by default when import/export Comhon objects or Comhon arrays

The validation might be done manually with two function :

  • validate : throw an exception if validation is not satisfied.
  • isValid : return true if validation is satisfied, false otherwise.
$myArray->isValid();
$myArray->validate();

$myObject->isValid();
$myObject->validate();

$myObject->getPorperty('firstName')->isValid('john');
$myObject->getPorperty('firstName')->validate('*fgf/*d-g/-');

Note : the manual validation is not "deep", it validate only current object, array or property.

If you want to validate your object or array "deeply" by validating nested objects you have to use object validator. During "deep" validation, if object is not "deeply" valid, an exception is thrown with informations of the first validation not satisied encountered.

$visitor = new ObjectValidator();
$visitor->execute($myObject);

If you want, object validator permit to :

  • verify if foreign objects have a complete id
  • verify if foreign objects are referenced (i.e. if there is same instance object on a NOT foreign property)
$visitor = new ObjectValidator();
$visitor->execute($myArray, [ObjectValidator::VERIF_REFERENCES => true, ObjectValidator::VERIF_FOREIGN_ID => true]);

Object serialization

Serialization

Whatever is your serialzation you just have to call one function save() to serialize your object. Obviously the model associated to your object must have a defined serialization.

Let's comhon find the operation : create or update (not patch)

$object->save();

provide the operation

$object->save(SerializationUnit::CREATE);

Note : Properties defined as not serializable and aggregations properties are not serialized.

Operations

There are three available operation for serialization : create, update or patch an object.

Create

The create operation permit to create a new object (create a file or add a row in SQL table...). The associated constant is :

\Comhon\Serialization\SerializationUnit::CREATE

Update

The update operation permit to update completely an object. All values are exported even if they are not updated. A file would be entirely replaced and each column of a SQL table row would be replaced (if a value is not set in instanciated comhon object, null is set in associated SQL table row's column). The associated constant is :

\Comhon\Serialization\SerializationUnit::UPDATE

Patch

The patch operation permit to update partially an object. Only updated values or/and casted objects are exported. Patch is not available for file serialization. For SQL table only wanted columns will be updated (if a value is not set AND flagged as updated in instanciated comhon object, null is set in associated SQL table row's column). The associated constant is :

\Comhon\Serialization\SerializationUnit::PATCH

Note : only root object might be partial, nested objects must be complete.

Specific case for SQL database

Your table has an incremental id

You can specify an operation but if you call save() function without operation, there are two cases to identify :

  1. if your Object doesn't have id set, operation create is chosen and an INSERT will be executed. if INSERT is successfull, id will be set in your object.
  2. if Object has id set, operation update (not patch) is chosen and an UPDATE will be executed

Your table doesn't have an incremental id

you must specify your operation (insert, update or patch)

// (SqlTable extends from SerializationUnit so constant is accessible from SqlTable)
$object->save(SqlTable::INSERT);
$object->save(SqlTable::UPDATE);
$object->save(SqlTable::PATCH);

Deserialization

Whatever is your serialzation you just have to call a simple function to deserialize your object. Obviously the model associated to your object must have a serialization.

Via model

$model = ModelManager::getInstance()->getInstanceModel('a_model_name');
$object = $model->loadObject('an_id');

// if your model has several id properties you must specify them in a json encoded array 
// order of values must be the same as order of id properties in manifest
$object = $modelTwo->loadObject('["an_id_1","an_id_2"]');

When you load an object, this object is stored in MainObjectCollection. And thanks to MainObjectCollection if an object has already been loaded it will not be loaded again (except if you force it).

// load object
$object = $model->loadObject(12);

// return same instance object retrieve from MainObjectCollection
$object = $model->loadObject(12);

// like previous instruction but force to call deserialization 
// and merge current object instance with serialized object
$object = $model->loadObject(12, true);

Via object

$object = new ComhonObject('a_model_name', false); // second parameter flag object as NOT loaded
$object->setId('an_id');
$object->load(); // return true if object is successfully loaded

You can filter properties during deserialization (with first parameter).
If object is flagged as loaded, deserialization is not launched except if you force it (with second parameter).

Via object value

When an object is loaded, foreign properties of this object that have their own serialization are instanciate but are not loaded. Like in previous section, if a value has already been loaded it will not be loaded again (except if you force it).

For example you have loaded a Test\Person, values like mother or children are instanciated but are not loaded.

$model = ModelManager::getInstance()->getInstanceModel('Test\Person');
$person = $model->loadObject(1);
echo $person->isLoaded(); // output true

echo $person->getValue('mother')->isLoaded(); // output false
echo $person->getValue('children')->isLoaded(); // output false

Unique value

To load a unique value call loadValue function :

$person->loadValue('mother');
echo $person->getValue('mother')->isLoaded(); // output true

Aggregation value

  • To fully load an aggregation value call loadValue function :
$person->loadValue('children');
echo $person->getValue('children')->isLoaded(); // output true

foreach ($person->getValue('children') as $child) {
    // each child has all values set, and is loaded
    echo $child->isLoaded(); // output true
}
  • To load only aggregation ids call loadAggregationIds function :
$person->loadAggregationIds('children');
echo $person->getValue('children')->isLoaded(); // output true

foreach ($person->getValue('children') as $child) {
    // each child has id set, but is not loaded
    echo $child->isLoaded(); // output false
}

Database time zone

For most (all?) SGBD connection you can define your time zone. to set a specific time zone for your connection you must specify it in file config.json (by default time zone connection is UTC).
Whatever is your database time zone you don't have to take care of it, serialization/deserialization will automatically affect good time zone to all your values with model dateTime.

Object interfacing

Interfacing consist to import entry in specific format to populate a comhon object or conversely export comhon object to specific format.

Interfacer

Comhon! provide three interfacer :

interfacer handled format handled parsed format
StdObjectInterfacer JSON or YAML stdClass
AssocArrayInterfacer JSON or YAML array
XMLInterfacer XML DOMElement

You may instanciate interfacer in two ways :

  • Call Interfacer::getInstance
// get AssocArrayInterfacer instance by providing true as second parameter
// and handle json format by providing 'json' as first parameter
$interfacer = Interfacer::getInstance('json', true);

// get StdObjectInterfacer instance by providing false as second parameter (default false)
// and handle yaml format by providing 'yaml' as first parameter
$interfacer = Interfacer::getInstance('yaml', false);

// get XMLInterfacer instance by providing 'xml' as first parameter
$interfacer = Interfacer::getInstance('xml');
  • Call constructors
$interfacer = new AssocArrayInterfacer('yaml', true);
$interfacer = new StdObjectInterfacer('json');
$interfacer = new XMLInterfacer();

You may provide mime type instead of format :

  • json: application/json
  • xml: application/xml
  • yaml: application/x-yaml

All interfacers extends Interfacer so they are very similar and have the same management. We will give example with a random interfacer but keep in mind that every examples are possible with all interfacers.

export

Let's start with an example

// create object that will be exported and add simple values
$person = new ComhonObject('Test\Person\Man');
$person->setValue('id', 1); // by setting id, object is added in MainObjectCollection
$person->setValue('firstName', 'John');
$person->setValue('lastName', 'Doe');

// create and add mother value
$woman = new ComhonObject('Test\Person\Woman');
$woman->setValue('id', 2); // by setting id, object is added in MainObjectCollection
$woman->setValue('firstName', 'Jane');
$woman->setValue('lastName', 'Doe');
$person->setValue('mother', $woman);

// create and add children values
$childOne = new ComhonObject('Test\Person\Woman');
$childOne->setValue('id', 3); // by setting id, object is added in MainObjectCollection

$childTwo = new ComhonObject('Test\Person\Man');
$childTwo->setValue('id', 4); // by setting id, object is added in MainObjectCollection

$children = $person->initValue('children');
$children->pushValue($childOne);
$children->pushValue($childTwo);

// create and add bodyArts values
$tattooOne = new ComhonObject('Test\Person\Tattoo');
$tattooOne->setValue('type', 'dragon');
$tattooOne->setValue('location', 'back');

$tattooTwo = new ComhonObject('Test\Person\Tattoo');
$tattooTwo->setValue('type', 'flower');
$tattooTwo->setValue('location', 'arm');

$bodyArts = $person->initValue('bodyArts');
$bodyArts->pushValue($tattooOne);
$bodyArts->pushValue($tattooTwo);

Now object is built, we can export it

$XMLInterfacer = new XMLInterfacer();

// via interfacer
$exportedPerson = $XMLInterfacer->export($person); // return instance of DOMNode

// via comhon object
$exportedPerson = $person->export($XMLInterfacer); // return instance of DOMNode

// via model (be careful when you use this method, $model and $person model must match)
$exportedPerson = $model->export($person, $XMLInterfacer); // return instance of DOMNode

// stringify exported object
$xml = $XMLInterfacer->toString($exportedPerson); // return XML string
echo $xml;

output :

<root id="1" firstName="John" lastName="Doe">
    <bodyArts>
        <bodyArt type="dragon" location="back" inheritance-="Test\Person\Tattoo"/>
        <bodyArt type="flower" location="arm" inheritance-="Test\Person\Tattoo"/>
    </bodyArts>
    <mother>2</mother>
    <children>
        <child id="3" inheritance-="Test\Person\Man"/>
        <child id="4" inheritance-="Test\Person\Woman"/>
    </children>
</root>

Foreign properties export

As you can see in previous example, value mother is exported as a simple id even if mother is actually an object. Foreign properties are exported like that for a simple reason, value can be defined elsewhere, and if we describe all object, same value would be described several times, it can be ambiguous and dangerous. If exported object had several id properties, it would be exported has JSON encoded array.
Children are foreign too, but exported node is not a simple id value, we will explain why in following section.

Inheritance

Object properties

Like described in manifest person example bodyArts property has Test\Person\BodyArt unique model. And In previous example we have added tattoos objects. Actually we can add tattoo because it inherit from Test\Person\BodyArt. So interfaced object tattoo must include it model name otherwise we would not be able to recognize if object is a tattoo, a piercing or simply a bodyArt. The object model name is specified under the key inheritance-.

Foreign properties

Like described in manifest person example children property has Test\Person unique model. And In previous example we have added Test\Person\Woman and Test\Person\Man objects. Actually we can add them because they inherit from Test\Person. Like "normal" object properties we have to specify model name, so instead of export only an id, we have to export an object that contain inheritance- and id. Note that id is always id, even if a model would have an id named "my_id" or several id.

Object root

The root object may be exported via a parent model. For example an object with model Test\Person\Woman may be exported via Test\Person model. The key inheritance- is added when exporting from parent model.

// create object that will be exported and add simple values
$woman = new ComhonObject('Test\Person\Woman');
$woman->setValue('id', 1);
$woman->setValue('firstName', 'Jane');
$woman->setValue('lastName', 'Doe');

// create and add mother value
$mother = new ComhonObject('Test\Person\Woman');
$mother->setValue('id', 2);
$woman->setValue('mother', $mother);

// export via parent model
$modelPerson = ModelManager::getInstance()->getInstanceModel('Test\Person');
$exportedPerson = $modelPerson->export($woman, $XMLInterfacer); // return instance of DOMNode

// stringify exported object
$xml = $XMLInterfacer->toString($exportedPerson); // return XML string
echo $xml;

output :

<root id="1" firstName="Jane" lastName="Doe" inheritance-="Test\Person\Woman">
    <mother>2</mother>
</root>

Import

$model = ModelManager::getInstance()->getInstanceModel('Test\Person');
$stdOject = json_decode('{"id":1,"firstName":"John"}');

$stdInterfacer = new StdObjectInterfacer();

// via interfacer
$importedPerson = $stdInterfacer->import($stdOject, $model); // return instance of ComhonObject
echo $importedPerson->getValue('firstName'); // output 'John'

// via model
$importedPerson = $model->import($stdOject, $stdInterfacer); // return instance of ComhonObject
echo $importedPerson->getValue('firstName'); // output 'John'

// via comhon object
$person = new ComhonObject($model);
$person->setValue('lastName', 'Doe');
$person->fill($stdOject, $stdInterfacer); // fill current instance of ComhonObject
echo $person->getValue('firstName'); // output 'John'
echo $person->getValue('lastName'); // output 'Doe'

Foreign properties import

When a value corresponding to a foreign property is imported, it has only id information and optionally inheritance- information (seen in previous section). Import will automatically try to find if referenced object exists in current object or in Main Object Collection and get this instance, otherwise it will create a new object.
To explain described behavior we add an id property id to Test\Person\BodyArt and we add a foreign property foreignTattoo with model Test\Person\Tattoo to Test\Person. It doesn't make sens in model but do not take this into account, it's just an example.

$model = ModelManager::getInstance()->getInstanceModel('Test\Person');
$stdOject = json_decode('{
    "id": 10,
    "firstName": "John",
    "lastName": "Doe",
    "mother": 20,
    "children": [
        {"inheritance-": "Test\Person\Man","id": 30},
        {"inheritance-": "Test\Person\Woman","id": 40}
    ],
    "bodyArts": [
        {"inheritance-":"Test\Person\Tattoo","id":1,"type":"dragon","location":"back"},
        {"inheritance-":"Test\Person\Tattoo","id":2,"type":"flower","location":"arm"}
    ],
    "foreignTattoo":1
}');

$woman = new ComhonObject($model);
$woman->setValue('id', 20); // by setting id, object is added in MainObjectCollection
$woman->setValue('firstName', 'Jane');
$woman->setValue('lastName', 'Doe');

$stdInterfacer = new StdObjectInterfacer();

$importedPerson = $stdInterfacer->import($stdOject, $model); // return instance of ComhonObject
echo $importedPerson->getValue('children')->getValue(1)->getValue('id');
// output 40

echo $importedPerson->getValue('mother')->getValue('firstName');
// output 'Jane'
echo $woman === $importedPerson->getValue('mother') ? 'same instance' : 'different instance';
// output 'same instance'

echo $importedPerson->getValue('foreignTattoo')->getValue('type');
// output 'dragon'
echo $importedPerson->getValue('foreignTattoo') === $importedPerson->getValue('bodyArts')->getValue(0)
        ? 'same instance' : 'different instance';
// output 'same instance'

Array Interfacing

You can interface (import and export) comhon arrays like comhon objects.

// create comhon array that will be exported
$persons = new ComhonArray('Test\Person');

// create and add person values
$woman = new ComhonObject('Test\Person\Woman');
$woman->setValue('id', 3);
$woman->setValue('firstName', 'Jane');
$woman->setValue('lastName', 'Doe');

$man = new ComhonObject('Test\Person\Man');
$man->setValue('id', 4);
$man->setValue('firstName', 'John');
$man->setValue('lastName', 'Doe');

$persons->pushValue($woman);
$persons->pushValue($man);

Now comhon array is built, we can export it

$XMLInterfacer = new XMLInterfacer();

// via interfacer
$exportedPersons = $XMLInterfacer->export($persons); // return instance of DOMNode

// via comhon object
$exportedPersons = $persons->export($XMLInterfacer); // return instance of DOMNode

// via model array (be careful when you use this method, $model and $persons model must match)
$exportedPersons = $model->export($persons, $XMLInterfacer); // return instance of DOMNode

// stringify exported object
$xml = $XMLInterfacer->toString($exportedPersons); // return XML string
echo $xml;

output :

<root>
    <person id="3" firstName="Jane" lastName="Doe" inheritance-="Test\Person\Woman"/>
    <person id="4" firstName="John" lastName="Doe" inheritance-="Test\Person\Man"/>
</root>

Interfacing preferences

You can define some preferences to apply during interfacing. All preferences have their constantes defined in Interfacer class to permit you to find them easily.

there is two ways to define interfacer preferences :

  • by calling setter :
$stdInterfacer = new StdObjectInterfacer();
$stdInterfacer->setPrivateContext(true);
$stdInterfacer->setDateTimeZone('UTC');
  • during interfacing :
$stdInterfacer = new StdObjectInterfacer();
$preferences = [
    Interfacer::PRIVATE_CONTEXT => true,
    Interfacer::DATE_TIME_ZONE => 'UTC',
];
$stdObject = $stdInterfacer->export($person, $preferences);

If you don't specify preference, the default preference will be used.
If you use same instance of interfacer for several import/export, preferences previously set will be used (or default value)

$stdInterfacer = new StdObjectInterfacer();
// default value for 'privateContext' is boolean false
$stdObject = $stdInterfacer->export($person); // export is NOT in private context
$stdObject = $stdInterfacer->export($person, ['privateContext' => true]); // export is in private context
$stdObject = $stdInterfacer->export($person); // export still in private context

privateContext

Summary

Define private context (context might be private or public).
Private properties are interfaced only in private context.

Description

type boolean
default false
availability import and export
constant Interfacer::PRIVATE_CONTEXT

Example

// here we suppose 'id' is a private property 
$person = new ComhonObject('Test\Person');
$person->setValue('id', 1);
$person->setValue('firstName', 'John');

$stdInterfacer = new StdObjectInterfacer();

$stdObject = $stdInterfacer->export($person);
echo $stdInterfacer->toString($stdObject);
// output '{"firstName":"John"}'

$stdObject = $stdInterfacer->export($person, [Interfacer::PRIVATE_CONTEXT => true]);
echo $stdInterfacer->toString($stdObject);
// output '{"id":1,"firstName":"John"}'

$stdObject = json_decode({"id":2,"firstName":"Jane"});
$person = $stdInterfacer->import($stdObject, $model, [Interfacer::PRIVATE_CONTEXT => false]);
echo $person->hasValue('id'); // output false
echo $person->getValue('Jane'); // output 'Jane'

$person = $stdInterfacer->import($stdObject, $model, [Interfacer::PRIVATE_CONTEXT => true]);
echo $person->getValue('id'); // output 2
echo $person->getValue('Jane'); // output 'Jane'

serialContext

Summary

Define serial context. This preference is used implicitly for serialization/deserialization.
Properties are interfaced with their serialization name.
Aggregation properties are not interfaced in serial context.
If exists, interface inheritance value as defined in serialization manifest.

Description

type boolean
default false
availability import and export
constant Interfacer::SERIAL_CONTEXT

Example

// here we suppose 'children' is an aggregation property 
// and property 'firstName' has serialization name 'first_name'
$person = new ComhonObject('Test\Person\Woman');
$person->setValue('firstName', 'John');

$childOne = new ComhonObject('Test\Person');
$childOne->setValue('id', 2);

$childTwo = new ComhonObject('Test\Person');
$childTwo->setValue('id', 3);

$children = $person->initValue('children');
$children->pushValue($childOne);
$children->pushValue($childTwo);

$stdInterfacer = new StdObjectInterfacer();

$stdObject = $stdInterfacer->export($person);
echo $stdInterfacer->toString($stdObject);
// output '{"firstName":"John","children":[2,3]}'

$stdObject = $stdInterfacer->export($person, [Interfacer::SERIAL_CONTEXT => true]);
echo $stdInterfacer->toString($stdObject);
// output '{"first_name":"John","gender":"Test\\Person\\Woman"}'

dateTimeFormat

Summary

Define date time format.
dateTime values will be exported in specified date time format.

Description

type string
default ISO 8601
availability export
constant Interfacer::DATE_TIME_FORMAT

Example

$person = new ComhonObject('Test\Person');
$person->setValue('firstName', 'John');
$birthDate = new ComhonDateTime('1988-09-16 16:30', new DateTimeZone('Europe/Paris'));
$person->setValue('birthDate', $birthDate);

$stdInterfacer = new StdObjectInterfacer();

$stdObject = $stdInterfacer->export($person);
echo $stdInterfacer->toString($stdObject);
// output '{"firstName":"John","birthDate":"1988-09-16T16:30:00+02:00"}'

$stdObject = $stdInterfacer->export($person, [Interfacer::DATE_TIME_FORMAT => 'Y-m-d H:i']);
echo $stdInterfacer->toString($stdObject);
// output '{"firstName":"John","birthDate":"1988-09-16 16:30"}'

dateTimeZone

Summary

Define date time zone.
dateTime values will be interfaced in specified date time zone.

Description

type string
default your PHP time zone
availability import and export
constant Interfacer::DATE_TIME_ZONE

Example

$person = new ComhonObject('Test\Person');
$person->setValue('firstName', 'John');
$birthDate = new ComhonDateTime('1988-09-16 16:30', new DateTimeZone('Europe/Paris'));
$person->setValue('birthDate', $birthDate);

$stdInterfacer = new StdObjectInterfacer();

$stdObject = $stdInterfacer->export($person, [Interfacer::DATE_TIME_ZONE => 'Europe/Paris']);
echo $stdInterfacer->toString($stdObject);
// output '{"firstName":"John","birthDate":"1988-09-16T16:30:00+02:00"}'

$stdObject = $stdInterfacer->export($person, [Interfacer::DATE_TIME_ZONE => 'UTC']);
echo $stdInterfacer->toString($stdObject);
// output '{"firstName":"John","birthDate":"1988-09-16T14:30:00+00:00"}'

// import dateTime that doesn't specify it time zone (Y-m-d H:i:s)
$stdObject = json_decode('{"firstName":"John","birthDate":"1988-09-16 16:30:00"}');

$person = $stdInterfacer->import($stdObject, $model, [Interfacer::DATE_TIME_ZONE => 'Europe/Paris']);
echo $person->getValue($stdObject)->format('c');
// output '1988-09-16T16:30:00+02:00' (+02:00 => 'Europe/Paris')


$person = $stdInterfacer->import($stdObject, $model, [Interfacer::DATE_TIME_ZONE => 'UTC']);
echo $person->getValue($stdObject)->format('c');
// output '1988-09-16T16:30:00+00:00' (+00:00 => 'UTC')

updatedValueOnly

Summary

Define if interfacer have to export only updated values (ids are always exported even if they are not updated).
This preference is applied only on current exported object. If there are "sub"-objects in current exported object, all values (of these sub-objects) will be exported even if they are not flagged as updated. This preference is used typically for sql serialization.

Description

type boolean
default false
availability export
constant Interfacer::ONLY_UPDATED_VALUES

Example

$person = new ComhonObject('Test\Person');
// third parameter of setValue() force status of value
// if we specify false, value we be considered as not updated
$person->setValue('id', 1, false);
$person->setValue('firstName', 'John');
$person->setValue('lastName', 'Doe', false);

$stdInterfacer = new StdObjectInterfacer();

$stdObject = $stdInterfacer->export($person, [Interfacer::ONLY_UPDATED_VALUES => true]);
echo $stdInterfacer->toString($stdObject);
// output '{"id":1,"firstName":"John"}'
// lastName is not exported, and id is exported even if it's not updated

propertiesFilters

Summary

Define a filter of values that will be exported.

Description

type array
default null
availability export
constant Interfacer::PROPERTIES_FILTERS

Example

$person = new ComhonObject('Test\Person');
$person->setValue('id', 1);
$person->setValue('firstName', 'John');
$person->setValue('lastName', 'Doe');
$person->setValue('age', 21);

$stdInterfacer = new StdObjectInterfacer();
$filter = ['firstName', 'age'];

$stdObject = $stdInterfacer->export($person, [Interfacer::PROPERTIES_FILTERS => $filter]);
echo $stdInterfacer->toString($stdObject);
// output '{"id":1,"firstName":"John","age":"21"}'
// lastName is not exported
// id is automatically added even if it's not in filter

flattenValues

Summary

Define if interfaced complex values (object or array) are flattened (stringified).
This preference is used typically during sql serialization.

Description

type boolean
default false
availability export and import
constant Interfacer::FLATTEN_VALUES

Example

$person = new ComhonObject('Test\Person');
$person->setValue('id', 1);
$person->setValue('firstName', 'John');

$tattooOne = new ComhonObject('Test\Person\Tattoo');
$tattooOne->setValue('type', 'dragon');
$tattooOne->setValue('location', 'arm');

$bodyArts = $person->initValue('bodyArts');
$bodyArts->pushValue($tattooOne);

$stdInterfacer = new StdObjectInterfacer();

$stdObject = $stdInterfacer->export($person, [Interfacer::FLATTEN_VALUES => true]);
echo $stdInterfacer->toString($stdObject);
// output '{"id":1,"firstName":"John","bodyArts":"[{\"type\":\"dragon\",\"location\":\"arm\",\"inheritance-\":\"Test\Person\Tattoo\"}]"}'

stringifiedValues

Summary

Define if all interfaced values are stringified.

Description

type boolean
default false
availability export and import
constant Interfacer::STRINGIFIED_VALUES

Example

$person = new ComhonObject('Test\Person');
$person->setValue('id', 1);
$person->setValue('firstName', 'John');

$tattooOne = new ComhonObject('Test\Person\Tattoo');
$tattooOne->setValue('type', 'dragon');
$tattooOne->setValue('location', 'arm');

$bodyArts = $person->initValue('bodyArts');
$bodyArts->pushValue($tattooOne);

$stdInterfacer = new StdObjectInterfacer();

$stdObject = $stdInterfacer->export($person, [Interfacer::STRINGIFIED_VALUES => true]);
echo $stdInterfacer->toString($stdObject);
// output '{"id":"1","firstName":"John","bodyArts":"[{\"type\":\"dragon\",\"location\":\"arm\",\"inheritance-\":\"Test\Person\Tattoo\"}]"}'
// as you can see id is exported as string even if it is defined a integer

flagValuesAsUpdated

Summary

Define if imported values will be flagged as updated.

Description

type boolean
default true
availability import
constant Interfacer::FLAG_VALUES_AS_UPDATED

Example

$stdInterfacer = new StdObjectInterfacer();

$stdObject = json_decode('{"id":1,"firstName":"John","bodyArts":[{"type":"dragon","location":"arm","inheritance-":"Test\Person\Tattoo"}]}');
$person = $stdInterfacer->import($stdObject, $model);
echo $person->isUpdatedValue('firstName'); // output true
echo $person->getValue('bodyArts')->getValue(0)->isUpdatedValue('type'); // output true

$stdObject = json_decode('{"id":2,"firstName":"Jane","bodyArts":[{"type":"dragon","location":"arm","inheritance-":"Test\Person\Tattoo"}]}');
$person = $stdInterfacer->import($stdObject, $model, [Interfacer::FLAG_VALUES_AS_UPDATED => false]);
echo $person->isUpdatedValue('firstName'); // output false
echo $person->getValue('bodyArts')->getValue(0)->isUpdatedValue('type'); // output false

flagObjectAsLoaded

Summary

Define if imported object will be flagged as loaded.

Description

type boolean
default true
availability import
constant Interfacer::FLAG_OBJECT_AS_LOADED

Example

$stdInterfacer = new StdObjectInterfacer();

$stdObject = json_decode('{"id":1,"firstName":"John","bodyArts":[{"type":"dragon","location":"arm","inheritance-":"Test\Person\Tattoo"}]}');
$person = $stdInterfacer->import($stdObject, $model);
echo $person->isLoaded(); // output true
echo $person->getValue('bodyArts')->getValue(0)->isLoaded(); // output true

$stdObject = json_decode('{"id":2,"firstName":"Jane","bodyArts":[{"type":"dragon","location":"arm","inheritance-":"Test\Person\Tattoo"}]}');
$person = $stdInterfacer->import($stdObject, $model, [Interfacer::FLAG_OBJECT_AS_LOADED => false]);
echo $person->isLoaded(); // output false
echo $person->getValue('bodyArts')->getValue(0)->isLoaded(); // output true

validate

Summary

Define if interfaced object must be validated or not. This setting concern only root object or root array and these first level elements. Any "deeper" objects are automatically validated anyway.

Description

type boolean
default true
availability export and import
constant Interfacer::VALIDATE

Example

We suppose a model Test\Person with a property 'lastName' defined as required.

$stdInterfacer = new StdObjectInterfacer();

// no validation
$person = $stdInterfacer->import($stdObject, $model, [Interfacer::VALIDATE => false]);

// validation
$stdObject = json_decode('{"id":1,"firstName":"John"}');
$person = $stdInterfacer->import($stdObject, $model, [Interfacer::VALIDATE => true]); // an exception is thrown

mergeType

Summary

There are two merge type :

  • merge :
    If imported object has id, try to find instance of comhon object with same id in MainObjectCollection.
    • If found, get existing comhon object and add imported values to existing values.
      • If an object value without id properties already exists, object is replaced by new instance.
      • If an object value with id properties already exists and has same id(s), object instance is kept but values are reset.
    • otherwise create new instance and populate it.
  • overwrite :
    If imported object has id, try to find instance of comhon object with same id in MainObjectCollection.
    • If found, get existing comhon object, delete existing values and populate it.
    • otherwise create new instance and populate it.

Description

type enumeration (1,2,3) (Interfacer::MERGE,Interfacer::OVERWRITE,Interfacer::NO_MERGE)
default Interfacer::MERGE
availability import
constant Interfacer::MERGE_TYPE

Example

$person = new ComhonObject('Test\Person');
$person->setValue('id', 1); // object is added to MainObjectCollection
$person->setValue('firstName', 'John');

$stdObject = json_decode('{"id":1,"lastName":"Doe"}');

$stdInterfacer = new StdObjectInterfacer();

$importedPerson = $stdInterfacer->import($stdObject, $model, [Interfacer::MERGE_TYPE => Interfacer::MERGE]);
echo $importedPerson->getValue('firstName'); // output 'John'
echo $importedPerson->getValue('lastName'); // output 'Doe'
echo $person === $importedPerson ? 'same instance' : 'different instance';
// output 'same instance'

$importedPerson = $stdInterfacer->import($stdObject, $model, [Interfacer::MERGE_TYPE => Interfacer::OVERWRITE]);
echo $importedPerson->hasValue('firstName'); // output false
echo $importedPerson->getValue('lastName'); // output 'Doe'
echo $person === $importedPerson ? 'same instance' : 'different instance';
// output 'same instance'

verifyReferences

Summary

Verify if foreign values in comhon object (export) or interfaced object (import) have existing reference in current object. Foreign values with main model are not concerned and may be interfaced without reference even if this setting is set to true. If a foreign value (not main) is not referenced, and setting is set to true, an exception is thrown.

Description

type boolean
default true
availability export and import
constant Interfacer::VERIFY_REFERENCES

Example

Like in Foreign properties import chapter, let's say we have a foreign property foreignTattoo with model Test\Person\Tattoo on model Test\Person.

$man = new ComhonObject('Test\Person\Man');
$father = $man->initValue('father');
$father->setId(1);
$bodyArts = $man->initValue('bodyArts');
$tattoo = new ComhonObject('Test\Person\Tattoo');
$tattoo->setId(1);
$bodyArts->pushValue($tattoo);
$man->setValue('foreignTattoo', $tattoo);

// tattoo with id '1' is referenced and father has main model
// so it works
$stdInterfacer = new StdObjectInterfacer();
$man->export($stdInterfacer);

$tattoo = new ComhonObject('Test\Person\Tattoo');
$tattoo->setId(10);
$man->setValue('foreignTattoo', $tattoo);

// tattoo with id '10' is not referenced
// so exception will be thrown
$stdInterfacer = new StdObjectInterfacer();
$man->export($stdInterfacer);
$man = new ComhonObject('Test\Person\Man');
$stdInterfacer = new StdObjectInterfacer();

// tattoo with id '1' is referenced and father has main model
// so it works
$stdObject = json_decode('{
    "bodyArts":[{"id":"1","inheritance-":"Test\\\\Person\\\\Tattoo"}], 
    "foreignTattoo":1,
    "mother":2
}');
$man->fill($stdObject, $stdInterfacer);

// tattoo with id '10' is not referenced
// so exception will be thrown
$stdObject = json_decode('{"foreignTattoo":10}');
$man->fill($stdObject, $stdInterfacer);

Duplicated objects

Duplicated objects are objects with same model or with share id and with same ids that are defined several times in same object. Duplicated objects are forbidden during object interfacing.

For Example let's say model Test\Person\Tattoo has an id property id. if we export a person with two tattoos that have same id, an exception will be thrown.

$man = new ComhonObject('Test\Person\Man');
$stdInterfacer = new StdObjectInterfacer();

$bodyArts = $man->initValue('bodyArts');
$tattooOne = new ComhonObject('Test\Person\Tattoo');
$tattooOne->setId(1);
$tattooTwo = new ComhonObject('Test\Person\Tattoo');
$tattooTwo->setId(1);
$bodyArts->pushValue($tattooOne);
$bodyArts->pushValue($tattooTwo);

// several tattoos with same id '1'
// so exception will be thrown
$man->export($stdInterfacer);

Isolated value

Isolated values feature permit to have duplicated objects values inside an object. For example if in model Test\Person, value bodyArt in array bodyArts was defined as isolated, we would be able to interface several body arts with same id (unlike in previous chapter). You may define a value as isolated in manifest, see isolated value chapter.

Exception management

An interfaced object may come from an unknown source and may contain errors (for example a REST API that accept JSON content to create resources). To avoid any irrelevant values, Comhon! import is strongly strict and may throw exception if any error is detected.

Import Exception is formatted to permit to know what kind of error is detected on which property. The import Exception thrown is always an instance of ImportException, has a code (one of code list), a message, and contain original exception. Actually import exception encapsulate original exception, and property where error is detected.

Example : we try to import a person with boolean value on a tattoo type property instead of string value.

$model = ModelManager::getInstance()->getInstanceModel('Test\Person');
$stdOject = json_decode('{"id":1,"bodyArts":[{"type":true,"inheritance-":"Test\\\\Person\\\\Tattoo"}]}');

$stdInterfacer = new StdObjectInterfacer();

try {
    $importedPerson = $stdInterfacer->import($stdOject, $model);
} catch(ImportException $e) {
    echo $e->getCode(); 
        // output => 203
    echo $e->getMessage(); 
        // output => value must be a string, boolean 'true' given
    echo json_encode($e->getStackProperties()); 
        // output => ["type",0,"bodyArts"] (note that stack start from property and rewind to root object)
    echo $e->getStringifiedProperties(); 
        // output => .bodyArts.0.type (note that string start from root object to final property)
    echo get_class($e->getOriginalException());
        // output => Comhon\Exception\UnexpectedValueTypeException
}

Object cast

Comhon! allow a kind of cast by affecting a new model to object. The new model must inherit from previous object model. For example you have instanciate an object with model Test\Person you might cast this object with model Test\Person\Woman. It's often used automatically with foreign properties and sql serialization.

Example

For this example we will assume that model Test\Person\Woman has an additional property than Test\Person. This property is maidenName.

$person = new ComhonObject('Test\Person');
$person->getModel()->getName(); // output 'Test\Person'
$person->setValue('maidenName', 'Doe'); // throw exception because 'maidenName' doesn't exist in model 'Test\Person'

$modelWoman = ModelManager::getInstance()->getInstanceModel('Test\Person\Woman');

$person->cast($modelWoman);
echo $person->getModel()->getName(); // output 'Test\Person\Woman'
$person->setValue('maidenName', 'Doe');
echo $person->getValue('maidenName'); // output 'Doe'

Limitation

If you have defined your own class Person and Woman. Model will be updated like we said before but Object will keep same class and it can be confusing.

$person = new Person();
$person->getModel()->getName(); // output 'Test\Person'
echo get_class($person); // output 'Your\Namespace\Person'

$modelWoman = ModelManager::getInstance()->getInstanceModel('Test\Person\Woman');

$person->cast($modelWoman);
echo $person->getModel()->getName(); // output 'Test\Person\Woman'
echo get_class($person); // output 'Your\Namespace\Person'

Automatic cast

As we have mentioned before, some cast might be made automatically. Let's take an example :
Like described in manifest person example, model Test\Person has a property bestFriend with model Test\Person (actually it might be a Test\Person\Man or a Test\Person\Woman). Objects with models Test\Person, Test\Person\Man and Test\Person\Woman are serialized in same sql table person. This table have a column best_friend_id (with integer type) that is a foreign key of person.id.
If we load a person we are not able to know if his best friend is a woman or a man except with an other database request. So best friend will be instanciated with model Test\Person.
If later we load best friend object, it will be automatically casted to Test\Person\Woman or a Test\Person\Man.

Here a simplified example of serialized persons :

id gender first_name last_name best_friend_id
1 Test\Person\Man john doe 2
2 Test\Person\Man john smith 1

First we load person with id 1 and then person with id 2

$modelPerson = ModelManager::getInstance()->getInstanceModel('Test\Person');
$modelPerson->loadObject(1);
echo $person->getValue('lastName'); // output 'doe'

echo $person->getValue('bestFriend')->isLoaded(); // output false
echo $person->getValue('bestFriend')->getModel()->getName(); // output 'Test\Person'
echo $person->getValue('bestFriend')->getValue('id'); // output 2
echo $person->getValue('bestFriend')->getValue('lastName'); // output NULL

// three equivalent ways to load 'bestFriend' value (first one is recommended) 
$person->loadValue('bestFriend');
$person->getValue('bestFriend')->load();
$modelPerson->loadObject($person->getValue('bestFriend')->getId());

echo $person->getValue('bestFriend')->isLoaded(); // output true
echo $person->getValue('bestFriend')->getModel()->getName(); // output 'Test\Person\Man'
echo $person->getValue('bestFriend')->getValue('lastName'); // output 'smith'

// bonus
echo $person === $person->getValue('bestFriend')->getValue('bestFriend') ? 'same instance' : 'different instance';
// output 'same instance'

In general, automatic casts are made during deserialization and import.

Clone this wiki locally