Skip to content

Class: Nether\Object\Prototype

Bob Magic II edited this page Aug 27, 2022 · 14 revisions

The Prototype Object a self-sealing stem object capable of translating schemas and ensuring that properties exist with default values if needed. Using this class as the parent enables the attribute based functionality all the way down.

$Object = new Nether\Object\Prototype(
	array|object|null $Input,
	array|object|null $Defaults,
	int|null $Flags
);

Usage Without Anything

Without any properties, attributes, or additional arguments, you get an object back with the properties you asked it to have. Here you can see MySQL gave us back a number as string and it will continue to be so after this. The result is basically if you had just done (object)$array except you get an object of the class of choice, in this case User.

class User
extends Nether\Object\Prototype {

}

$RowFromDB = [
	'user_id'=> '1',
	'user_name'=> 'bob',
	'user_email'=> 'bmajdak-at-php-dot-net',
	'user_title'=> 'Chief Iconoclast'
];

var_dump(new User($RowFromDB));
object(User)#1 (4) {
	["user_id"]=> string(1) "1"
	["user_name"]=> string(3) "bob"
	["user_email"]=> string(22) "bmajdak-at-php-dot-net"
	["user_title"]=> string(16) "Chief Iconoclast"
}

Usage With Typed Properties

Changing nothing except adding typed properties to the class will cause it to typecast the value for you. In the example where MySQL gave us back a string numeral '1' which will then be casted to (int)'1' for you. Only the core PHP types can be casted - mixed and classes/interfaces currently cannot be.

class User
extends Nether\Object\Prototype {

	public int $user_id;
	public string $user_name;
	public string $user_email;
	public string $user_title;

}

var_dump(new User($RowFromDB));
object(User)#2 (4) {
	["user_id"]=> int(1)
	["user_name"]=> string(3) "bob"
	["user_email"]=> string(22) "bmajdak-at-php-dot-net"
	["user_title"]=> string(16) "Chief Iconoclast"
}

Usage With Mapped Input

We all know that Dave asked absolutely nobody when he designed that database table, and now you are stuck having to type that snake-case crap the rest of your life. Using attributes the input data can be remapped to an API that will not damage your calm. Technically this makes it half of an ORM.

class User
extends Nether\Object\Prototype {

	#[Nether\Object\Meta\PropertyOrigin('user_id')]
	public int $ID;

	#[Nether\Object\Meta\PropertyOrigin('user_name')]
	public string $Name;

	#[Nether\Object\Meta\PropertyOrigin('user_email')]
	public string $Email;

	#[Nether\Object\Meta\PropertyOrigin('user_title')]
	public string $Title;

}

var_dump(new User($RowFromDB));
object(User)#3 (4) {
	["ID"]=> int(1)
	["Name"]=> string(3) "bob"
	["Email"]=> string(22) "bmajdak-at-php-dot-net"
	["Title"]=> string(16) "Chief Iconoclast"
}

Usage With Default Values

The second argument to the constructor is an array of default values that should be filled into the object if the data source was missing something. In this example our MySQL data had no user_status field.

class User
extends Nether\Object\Prototype {

	#[Nether\Object\Meta\PropertyOrigin('user_id')]
	public int $ID;

	#[Nether\Object\Meta\PropertyOrigin('user_name')]
	public string $Name;

	#[Nether\Object\Meta\PropertyOrigin('user_email')]
	public string $Email;

	#[Nether\Object\Meta\PropertyOrigin('user_title')]
	public string $Title;

	#[Nether\Object\Meta\PropertyOrigin('user_status')]
	public string $Status;

}

$Defaults = [
	'Status' => 'Probably Cool'
];

var_dump(new User($RowFromDB));
var_dump(new User($RowFromDB, $Defaults));
object(User)#4 (4) {
	["ID"]=> int(1)
	["Name"]=> string(3) "bob"
	["Email"]=> string(22) "bmajdak-at-php-dot-net"
	["Title"]=> string(16) "Chief Iconoclast"
	["Status"]=> uninitialized(string)
}

object(User)#5 (5) {
	["ID"]=> int(1)
	["Name"]=> string(3) "bob"
	["Email"]=> string(22) "bmajdak-at-php-dot-net"
	["Title"]=> string(16) "Chief Iconoclast"
	["Status"]=> string(13) "Probably Cool"
}

Usage With Additional Flags

The third argument to the constructor is a flagset that can change some of the behaviours during construction. By default if properties have not been directly mapped in the class the additional ones will be added on the fly, which makes them both public and mixed type. Using the Strict flags you can have it only include the properties that are hardcoded into the class definition.

use Nether\Object\Prototype;
use Nether\Object\Prototype\Flags;
use Nether\Object\Meta\PropertyOrigin;

class User
extends Prototype {

	#[PropertyOrigin('user_id')]
	public int $ID;

	#[PropertyOrigin('user_name')]
	public string $Name;

}

var_dump(new User(
	$RowFromDB,
	$Defaults
));

var_dump(new User(
	$RowFromDB,
	$Defaults,
	(Flags::StrictInput | Flags::StrictDefault)
));
object(User)#6 (4) {
	["ID"]=> int(1)
	["Name"]=> string(3) "bob"
	["Status"]=> string(13) "Probably Cool"
	["user_email"]=> string(22) "bmajdak-at-php-dot-net"
	["user_title"]=> string(16) "Chief Iconoclast"
}

object(User)#7 (2) {
	["ID"]=> int(1)
	["Name"]=> string(3) "bob"
}

Usage Auto Instantiating Simple Objects

Properties of object types that do not require anything fancy passed to the constructors can be instantiated automatically using an attribute. Anything given to the attribute will be passed to the constructor.

The first example constructs a new Datastore with no arguments. The second example constructs a new Datastore giving it an array of data as its first argument.

use Nether\Object\Prototype;
use Nether\Object\Meta\PropertyObjectify;

class One
extends Prototype {

	#[PropertyObjectify]
	public Nether\Object\Datastore $List;

}

class Two
extends Prototype {

	#[PropertyObjectify(['one','two','three'])]
	public Nether\Object\Datastore $List;

}

var_dump(new One);
var_dump(new Two);
object(One)#1 () {
	["List"]=> object(Nether\Object\Datastore)#5 (5) {
		["Data":protected]=> array(0) {
		}
	}
}
object(One)#1 () {
	["List"]=> object(Nether\Object\Datastore)#5 (5) {
		["Data":protected]=> array(3) {
			[0]=> string(3) "one"
			[1]=> string(3) "two"
			[2]=> string(5) "three"
		}
	}
}

Usage With Custom OnReady Method

When the Prototype constructor finishes it calls the OnReady method on that object. If more complex operations are needed to instantiate a new object then this would be a good place to do that.

OnReady methods take a single ConstructArgs argument that contains the Input, Defaults, and Flags from the original point of construction.

use Nether\Object\Datastore;
use Nether\Object\Prototype;
use Nether\Object\Prototype\ConstructArgs;

class Three
extends Prototype {

	public Datastore
	$List;

	protected function
	OnReady(ConstructArgs $Args):
	void {

		// build a new list, filter some values out, and sort it.

		$this->List = (
			(new Datastore([ 'one', 'two', 'three', 'four' ]))
			->Filter(fn($Val)=> ($Val !== 'three'))
			->Sort(fn($A, $B)=> ($A <=> $B))
		);

		return;
	}

}
object(One)#1 () {
	["List"]=> object(Nether\Object\Datastore)#5 (5) {
		["Data":protected]=> array(3) {
			[3]=> string(4) "four"
			[0]=> string(3) "one"
			[1]=> string(3) "two"
		}
	}
}