JSON_PROPERTY_MAP
JSON_PROPERTY_MAP
Tells us how to map JSON properties to internal PHP types or objects.
This constant can be overridden in your subclasses, to add custom JSON
mapping definitions to your class. It is recommended to always do so.
The value must be an array of key-value pairs, where the key is the name
of a JSON property and the value is a string with the definition for that
specific property in PHPdoc-style. We will then perform automatic, strict
lazy-conversion of the value to the target type whenever you access that
property. (However, be aware that we always allow NULL
values in any
property, and will never enforce/convert those to the target type.)
The following built-in types are supported: bool
, int
, float
,
string
. The JSON data will be type-converted to match that type.
Note that if the type is set to mixed
or an empty string ""
instead,
then the property will allow any of the built-in types (bool
, int
,
float
, string
, NULL
(as always) and multi-level arrays of any of
those types). There simply won't be any "forced" type-conversion to any
specific basic type for that property since you haven't defined any
strict type.
Setting up untyped properties is still useful even if you don't know its
type, since defining the property in the class map ensures that you can
always get/set/access/use that property even if it's unavailable in the
current object instance's internal JSON data.
You can also map values to objects. In the case of objects (classes), the
classes MUST inherit from LazyJsonMapper
so they support all necessary
mapping features. To assign a class to a property, you can either write a
FULL (global) class path starting with a leading backslash \
, such as
\Foo\Bar\Baz
. Alternatively, you can use a RELATIVE path related to the
namespace of the CURRENT MAP'S class, such as Bar\Baz
(if your current
class is \Foo\Whatever
, then it'd be interpreted as \Foo\Bar\Baz
).
Anything that DOESN'T start with a leading backslash is interpreted as a
relative class path! Just be aware that your class use
-statements are
completely ignored since PHP has no mechanism to let us detect those.
It's also possible to map properties to the core LazyJsonMapper
object
if you simply want an object-oriented container for its data without
writing a custom class. You can do that by defining the property type as
either \LazyJsonMapper\LazyJsonMapper
, or as LazyJsonMapper
(which is
a special shortcut that ALWAYS resolves to the core class). But it's
always best to write a proper class, so that its properties are reliable.
Lastly, you can map "arrays of TYPE" as well. Simply add one or more []
brackets to the end of the type. For example, int[]
means "array of
ints", and \Foo[][][]
means "array of arrays of arrays of \Foo
objects". It may be easier to understand those mentally if you read them
backwards and say the words "array of" every time you see a []
bracket.
(Note: You can also use the array notation with mixed[][]
, which would
then strictly define that the value MUST be mixed data at an exact depth
of 2 arrays, and that no further arrays are allowed deeper than that.)
The assigned types and array-depths are STRICTLY validated. That's an
integral part of the LazyJsonMapper
container, since it guarantees your
class property map interface will be strictly followed, and that you can
fully TRUST the data you're interacting with. If your map says that the
array data is at depth 8 and consists of YourObject
objects, then we'll
make sure the data is indeed at depth 8 and their value type is correct!
That goes for arrays too. If you define an int[]
array of ints, then
we'll ensure that the array has sequential numeric keys starting at 0 and
going up without any gaps, exactly as a JSON array is supposed to be. And
if you define an object YourObject
, then we'll ensure that the input
JSON data consists of an array with associative keys there (which is used
as the object properties), exactly as a JSON object is supposed to be.
In other words, you can TOTALLY trust your data if you've assigned types!
Example property map:
const JSON_PROPERTY_MAP = [
'some_string' => 'string',
'an_object' => '\YourProject\YourObject',
'array_of_numbers' => 'int[]',
'array_of_objects' => 'RelativeObject[]',
'untyped_value' => '', // shorthand for 'mixed' below:
'another_untyped_value' => 'mixed', // allows multilevel arrays
'deep_arr_of_arrs_of_int' => 'int[][]',
'array_of_mixed' => 'mixed[]', // enforces 1-level depth
'array_of_generic_objects' => 'LazyJsonMapper[]',
];
Also note that we automatically inherit all of the class maps from all
parents higher up in your object-inheritance chain. In case of a property
name clash, the deepest child class definition of it takes precedence.
It is also worth knowing that we support a special "multiple inheritance"
instruction which allows you to "import the map" from one or more other
classes. The imported maps will be merged onto your current class, as if
the entire hierarchy of properties from the target class (including the
target class' inherited parents and own imports) had been "pasted inside"
your class' property map at that point. And you don't have to worry
about carefully writing your inheritance relationships, because we have
full protection against circular references (when two objects try to
import each other) and will safely detect that issue at runtime whenever
we try to compile the maps of either of those bad classes with their
badly written imports.
The instruction for importing other maps is very simple: You just have to
add an unkeyed array element with a reference to the other class. The
other class must simply refer to the relative or full path of the class,
followed by ::class
.
Example of importing one or more maps from other classes:
const JSON_PROPERTY_MAP = [
'my_own_prop' => 'string',
OtherClass::class, // relative class path
'redefined_prop' => float,
\OtherNamespace\SomeClass::class, // full class path
];
The imports are resolved in the exact order they're listed in the array.
Any property name clashes will always choose the version from the latest
statement in the list. So in this example, our class would first inherit
all of its own parent (extends
) class maps. Then it would add/overwrite
my_own_prop
. Then it imports everything from OtherClass
(and its
parents/imports). Then it adds/overwrites redefined_prop
(useful if you
want to re-define some property that was inherited from the other class).
And lastly, it imports everything from \OtherNamespace\SomeClass
(and
its parents/imports). As long as there are no circular references,
everything will compile successfully and you will end up with a very
advanced final map! And any other class which later inherits from
(extends) or imports YOUR class will inherit the same advanced map from
you! This is REAL "multiple inheritance"! ;-)
Also note that "relative class path" properties are properly inherited as
pointing to whatever was relative to the original inherited/imported
class where the property was defined. You don't have to worry about that.
The only thing you should keep in mind is that we ONLY import the maps.
We do NOT import any functions from the imported classes (such as its
overridden functions), since there's no way for us to affect how PHP
resolves function calls to "copy" functions from one class to another. If
you need their functions, then you should simply extends
from the class
to get true PHP inheritance instead of only importing its map. Or you
could just simply put your functions in PHP Traits and Interfaces so
that they can be re-used by various classes without needing inheritance!
Lastly, here's an important general note about imports and inheritance:
You DON'T have to worry about "increased memory usage" when importing or
inheriting tons of other classes. The runtime "map compiler" is extremely
efficient and re-uses the already-compiled properties inherited from its
parents/imports, meaning that property inheritance is a ZERO-COST memory
operation! In fact, re-definitions are also zero-cost as long as your
class re-defines a property to the exact same type that it had already
inherited/imported. Such as Base: foo:string, Child: foo:string
. In
that case, we detect that the definitions are identical and just re-use
the compiled property from Base
instead. It's only when a class adds
NEW/MODIFIED properties that memory usage increases a little bit! In
other words, inheritance/imports are a very good thing, and are always a
better idea than manually writing similar lists of properties in various
unrelated (not extending each other) classes. In fact, if you have a
situation where many classes need similar properties, then it's a GREAT
idea to create special re-usable "property collection" classes and then
simply importing THOSE into ALL of the different classes that need those
sets of properties! You can even use that technique to get around PHP's
use
-statement limitations (simply place a "property collection" class
container in the namespace that had all of your use
-classes, and define
its properties via easy, relative paths there, and then simply make your
other classes import THAT container to get all of its properties).
As you can see, the mapping and inheritance system is extremely powerful!
Note that the property maps are analyzed and compiled at runtime, the
first time we encounter each class. After compilation, the compiled maps
become immutable (cannot be modified). So there's no point trying to
modify this variable at runtime. That's also partially the reason why
this is defined through a constant, since they prevent you from modifying
the array at runtime and you believing that it would update your map. The
compiled maps are immutable at runtime for performance & safety reasons!
Have fun!