A generic document class used to create structured data objects. One use for this is to create objects that conform to an API request or response.
composer require treehousetim/document
After installing, create your own class that extends treehousetim\document
.
Add all your properties as protected and implement the abstract methods.
Many times you want to be able to remap internal field names to hide implementation details. the document class provides a way to set values from an array that has different field names from your document.
public function setFromMappedArray( array $map, array $data )
Pass in a map array in the format ['incomingDataArrayKey' => 'documentClassFieldName']
see example at the bottom of this README
abstract public function jsonSerialize();
Return a structure that makes sense for your document. This method is called automatically if you json_encode an instance of your class.
abstract protected function validate()
This is semantic - you should call ->validate() where it makes sense in your code.
It is suggested to always call $this->validate();
at the top of your jsonSerialize()
function.
You can see one approach in the examples at the bottom.
This method will be called on sub documents that are members of a document.
After extending and creating a document class with properties, you can set values using chained function calls.
// See class definitions at the bottom of this README
// create a sub document and pass it to a function
$name = (new personName())
->first( 'Easter' )
->last( 'Bunny' )
->full( 'The Easter Bunny' );
$customer = new (customer() )
->name( $name );
// or create sub documents inline
$customer = new (customer() )
->name( (new personName())
->first( 'Easter' )
->last( 'Bunny' )
->full( 'The Easter Bunny' )
)
->address_line_1( '123 Main Street' )
->address_line_2( '' )
->city( 'Dubuque' )
->state_province( 'IA' )
->postal_code( '12345' );
You should always write a setter method to enforce the type of the sub document (if using PHP 7.4 or later, you can specify type on the class declaration).
see example at the bottom of this README
You can validate values coming into your document to conform to a list of allowed values by using $this->validateValueInList
.
class aDocument extends \treehousetim\document\document
{
const adtHTML = 'html';
const adtTEXT = 'text';
const adtJSON = 'json';
protected $allowedTypes = [
self::adtHTML,
self::adtTEXT,
self::adtJSON
];
protected $type;
public function type( string $type ) : self
{
$this->validateValueInList( 'type', $type, $this->allowedTypes );
return parent::type( $type );
}
//------------------------------------------------------------------------
public function jsonSerialize()
{
$this->validate();
return $this->getFieldArray( 'type' );
}
//------------------------------------------------------------------------
protected function validate()
{
$this->validateRequired( 'type' );
}
}
In order to support documents containing array lists of other documents, you can have properties/fields that are arrays.
class arrayDocument extends \treehousetim\document\document
{
protected $name = array();
public function jsonSerialize ()
{
$this->validate();
return $this->getFieldArray( 'name' );
}
//------------------------------------------------------------------------
public function name( nameDocument $nameDoc ) : self
{
$this->name[] = $nameDoc;
$this->markValueSet( 'name' );
return $this;
}
//------------------------------------------------------------------------
protected function validate()
{
$this->validateSubDocument( 'name' );
}
}
If you write a custom property setter as described above, you might need to call ->markValueSet( $name )
to ensure validation works.
If you don't call the parent class and set the property directly, you will need to call markValueSet( $name )
.
If you call the parent class like this you will not need to.
// choose one way to do this
public function name( personName $name ) : self
{
$this->name = $name;
$this->markValueSet( 'name' );
return $this;
}
// or
public function name( personName $name ) : self
{
return parent::name( $name );
}
See the examples at the bottom.
You can json_encode a document sub class and it will return what you return from jsonSerialize()
serialized into a JSON string.
This will return the result of ->jsonSerialize()
cast as an array.
This will return the result of ->jsonSerialize()
cast as an object. This will be a stdClass.
This will return a new object instance for the given $className
. This object will be created using property calls to set values.
Pass the optional map in the format of ['document_field'=>'object_field', ...]
This will return a new object instance for the given $className
. This object will be created using function calls using the form: ->prop_name( $value )
Pass the optional map in the format of ['document_field'=>'object_field', ...]
dataArray
and dataObject
will both be shallow arrays/objects - it only affects the return type of the immediate document, these do not descend into sub documents.
You can use built in methods in the document class to validate. Validation is done via exceptions - you should validate your data before creating documents if you want to return end-user validation messages.
A public interface to the protected validate method. Public/Protected are separated for possible future features.
Exceptions are thrown using codes according to the following list
class Exception extends \LogicException
{
const undefined = -1;
const missingData = 1;
const wrongType = 2;
const callOneVar = 3;
const noSuchProperty = 4;
const disallowedValue = 5;
const noValue = 6;
}
For properties that are set using the treehousetim\document\document
class, this validation rule will work. No matter the value set on the property, if it has been set using a function call this validation rule will succeed. No exception will be thrown.
If you are using custom property setting class functions, you will need to call ->markValueSet( 'property_name' );
in your function.
The code thrown is Exception::missingData
If the property does not exist, you will receive an exception with a code Exception::noSuchProperty
. If the property has not been set, you will receive an exception code Exception::missingData
.
You can validate a sub document using this method. Internally, ->validateRequired
is called first. Then if the class property is not a document subclass, you will receive an exception code Exception::wrontType
.
If both previous conditions pass, ->validate is called on the property, which is another document class and may throw other exceptions from its validation function.
Note: the call to ->validate works even though it is a protected method because that's how PHP works.
You can validate a value to be in a list using this method.
The code thrown is Exception::disallowedValue
The suggestion is made to implement a setter function for any values you want to set using this validation function. Before you set the value on the document class you would call this validation function. This will protect your document object from ever having wrong values set on it.
Set Example Above
Validates that a property exists and is not equal to null. !== null
The code thrown is Exception::noSuchProperty
if property does not exist on class.
The code thrown is Exception::missingData
if the property === null.
You can and should perform any other validations that are necessary to protect the integrity of your document's data. The suggestion is to throw exceptions - it should be a validation of last resort, not end user validation with error messages.
You can and should throw \treehousetim\document\Exception
exceptions along with an appropriate exception code from this project's Exception class.
class Exception extends \LogicException
{
const undefined = -1;
const missingData = 1;
const wrongType = 2;
const callOneVar = 3;
const noSuchProperty = 4;
const disallowedValue = 5;
const noValue = 6;
}
If you have cloned this repository, you can run the tests.
- Install test dependencies:
composer install
- Run tests:
composer test
<?PHP namespace application\libraries\documents;
class customer extends \treehousetim\document\document
{
protected $name;
protected $address_line_1;
protected $address_line_2;
protected $city;
protected $state_province;
protected $postal_code;
protected $email;
public function jsonSerialize ()
{
$this->validate();
return $this->getFieldArray(
'name',
'address_line_1',
'address_line_2',
'city',
'state_province',
'postal_code',
'email'
);
}
//------------------------------------------------------------------------
protected function validate()
{
// email is optional, so not included
$this->validateHasValue(
'address_line_1',
'city',
'state_province',
'postal_code'
);
$this->validateRequired( 'address_line_2' );
$this->validateSubDocument( 'name' );
}
//------------------------------------------------------------------------
public function name( personName $name ) : self
{
return parent::name( $name );
}
}
class personName extends \treehousetim\document\document
{
protected $first;
protected $last;
protected $full;
public function jsonSerialize()
{
return $this->getFieldArray(
'first',
'last',
'full'
);
}
//------------------------------------------------------------------------
protected function validate()
{
// all are required to be set with some data
$this->validateHasValue(
'first',
'last',
'full'
);
}
}
<?PHP
$map = ['fullName' => 'full_name', 'firstName' => 'first_name', 'lastName' => 'last_name'];
// could be from POST or API incoming parsed JSON
$data = ['fullName' => 'Robot Droid', 'firstName' => 'Robot', 'lastName' => 'Droid'];
// nameDocument from this project's unit tests
$doc = new \treehousetim\document\test\nameDocument();
$doc->setFromMappedArray( $map, $data );
// $doc now has the proper values set from the data array
<?php namespace App\Models;
class Names
{
public $full_name;
public $first_name;
public $last_name;
public $suffix;
}
<?PHP
use App\Models\Names as NameModel;
use App\Documents\Name as NameDocument;
$nameDoc = (new NameDocument())
->first_name( 'Robby' )
->last_name( 'Robot' )
->full_name( 'Robby the Robot' );
$model = $nameDoc->asClassWithProps( NameModel::class );
echo $model->first_name; // outputs Robby
<?php namespace App\Models;
class Names
{
protected $full_name;
protected $first_name;
protected $last_name;
protected $suffix;
public function __call( $name, $args )
{
$this->${name} = $args[0];
}
//------------------------------------------------------------------------
public function get_first_name() : string
{
return $this->first_name;
}
}
<?PHP
use App\Models\Names as NameModel;
use App\Documents\Name as NameDocument;
$nameDoc = (new NameDocument())
->first_name( 'Robby' )
->last_name( 'Robot' )
->full_name( 'Robby the Robot' );
$model = $nameDoc->asClassWithFuncs( NameModel::class );
$model->get_first_name(); // outputs Robby
protected function optionalFieldOut( $fieldName, array &$out )
protected function fieldOutIfSet( $name, array &$out )
protected function getFieldArray( string ...$fields ) : array
public function doValidate()
public function dataArray() : array
public function dataObject() : \stdClass
public function fieldExists( $name ) : bool
public function fieldSet( $name ) : bool
public function fieldExistsAndIsSet( $name ) : bool
public function asClassWithProps( string $className, $map = array() ) : object
public function asClassWithFuncs( string $className, $map = array() ) : object