yeast / loafpan
Object and config hydration from associative arrays
Requires
- brick/varexporter: ^0.3.5
- jawira/case-converter: ^3.4
- nette/php-generator: ^3.6
- symfony/polyfill-php81: ^1.24
Requires (Dev)
- jetbrains/phpstorm-attributes: ^1.0
- justinrainbow/json-schema: ^5.2
- phpstan/phpstan: ^1.4
- phpunit/phpunit: 9.5.13
- ramsey/uuid: >=4
- vimeo/psalm: ^4.19
Suggests
- ramsey/uuid: Allows parsing of UUID's in units
README
A simple PHP 8 native object expansion (or hydration as some call it) framework
Features
- Only focused on deserialization of data, thus perfect for configs
- JSON Schema generation (also works for yaml!)
- PHP 8 Attribute guided
- Simple templating/generics support
- Simple alternative format expanders
- Custom expander support allows for expanding out-of-branch objects
- Readable code generation, for ease of debugging and speed
Installation
composer require yeast/loafpan
Very quick use
$config = json_decode($json, true); $loafpan = new Loafpan($loafpanCacheDirectory); // Don't forget to annotate your class. $configClass = $loafpan->expand(MyConfig::class, $config);
Usage
To start using Loafpan, you first need to annotate your objects, in the next few examples we will create to fake classes to explain the details
An expandable object is called a "Unit", Units either need to be either annotated with Unit
or have a custom expander
registered. However for now we'll cover annotated Units
In the first example, Sandwich is a purely setter based unit, and will use the default constructor to instantiate the object and then set the properties manually.
Loafpan is here guided by the Field
attributes, which signal which properties can be applied from an object.
// The first argument of `Unit` is the description of the object // This is used in the JSON Schema generation #[Unit("This is a very nice sandwich")] class Sandwich { // A custom field name can be given with the `name` parameter #[Field(name: "title")] public string $name = ""; // since PHP has no native support for generics or typed arrays (yet) // one can override the type, and use list<T> to define the actual type #[Field("The toppings of this sandwich", type: 'list<Yeast\Demo\Topping>')] public array $toppings = []; }
Topping however is a purely expander based, and will take only a string, as this is the only instantiation method available
Expander functions are public static functions with either 1 or 2 arguments (the optional second one being a Loafpan instance), the first argument defines the input type that can be used to expand the object from, e.g. Topping can be made from a string alone
#[Unit("What goes on the bread stays on the bread")] class Topping { private function __construct(private bool $wet = false) {} #[Expander] public static function fromName(string $name) { return new static($name === 'water' ? true : false); } }
While Topping only defines 1 Expander, you can add multiple, be aware that the results can be unpredictable when the types overlap.
With the 2 classes we just defined, a valid json object for Sandwich would be
{ // because the "name" property has set it's name to "title", // the json object must use title "title": "Soggy sandwich", "toppings": [ "water", "2 pounds of lead" ] }
which will roughly translate into
$sandwich = new Sandwich(); $sandwich->name = "Soggy sandwich"; $sandwich->toppings = [ Topping::fromString("water"), Topping::fromString("2 pounds of lead") ];
Finally, applying all this, create a Loafpan instance with a directory in which it can scribble php files and
call expand
with the class you want to expand into, in our case Sandwich
and the user input.
The following example shows this usage
$loafpan = new Loafpan($yourLoafpanCacheDirectory); /** @var Sandwich $sandwich */ $sandwich = $loafpan->expand(Sandwich::class, [ "title" => "Soggy sandwich", "toppings" => [ "water", "2 pounds of lead" ] ]); echo "I have a sandwich called " . $sandwich->name . " the topping:\n"; foreach ($sandwich->toppping as $topping) { echo " - " . ($topping->wet ? 'wet' : 'not wet') . "\n"; }
If the options given here doesn't give you enough flexibility, you can always implement your own expander by
implementing UnitExpander
(\Yeast\Loafpan\UnitExpander
) on a class and either registering it to loafpan by using
the registerExpander
function on a Loafpan instance or setting the expander
parameter on the Unit
attribute
See src/Expander for some examples of custom UnitExpanders
Default expanders
list<T>
- only accepts an array list with items of type Tmap<T>
- only accepts an associative array with items of type TDateTime
/DateTimeImmutable
- only accepts a string with an ISO-8601 formatted dateRamsey\Uuid\Uuid
/Ramsey\Uuid\UuidInterface
- only accepts a string with a properly formatted UUID
Todo
- Generate in-depth errors about invalid input