ornament / core
PHP7 ORM toolkit, core package
Requires
- php: >=8.1
Requires (Dev)
- gentry/gentry: ^0.16.0
- toast/unit: ^2
Suggests
- monolyth/formulaic: PHP8 OO-forms with data binding for (Ornament) models
- dev-master
- 0.16.5
- 0.16.4
- 0.16.3
- 0.16.2
- 0.16.1
- 0.16.0
- 0.15.9
- 0.15.8
- 0.15.7
- 0.15.6
- 0.15.5
- 0.15.4
- 0.15.3
- 0.15.2
- 0.15.1
- 0.15.0
- 0.14.0
- 0.13.1
- 0.13.0
- 0.12.2
- 0.12.1
- 0.12.0
- 0.11.10
- 0.11.9
- 0.11.8
- 0.11.7
- 0.11.6
- 0.11.5
- 0.11.4
- 0.11.3
- 0.11.2
- 0.11.1
- 0.11.0
- 0.10.6
- 0.10.5
- 0.10.4
- 0.10.3
- 0.10.2
- 0.10.1
- 0.10.0
- 0.9.3
- 0.9.2
- 0.9.1
- 0.9.0
- 0.8.3
- 0.8.2
- 0.8.1
- 0.8.0
- 0.7.1
- 0.7.0
- 0.6.16
- 0.6.15
- 0.6.14
- 0.6.13
- 0.6.12
- 0.6.11
- 0.6.10
- 0.6.9
- 0.6.8
- 0.6.7
- 0.6.6
- 0.6.5
- 0.6.4
- 0.6.3
- 0.6.2
- 0.6.1
- 0.6.0
- 0.5.2
- 0.5.1
- 0.5.0
- 0.4.2
- 0.4.1
- 0.4.0
- 0.3.0
- 0.2.0
- 0.1.2
- 0.1.1
- 0.1.0
- 0.0.2
- 0.0.1
- dev-dependabot/composer/twig/twig-3.14.2
- dev-develop
This package is auto-updated.
Last update: 2024-12-12 20:59:44 UTC
README
PHP7 ORM toolkit, core package
ORM is a fickle beast. Many libraries (e.g. Propel, Doctrine, Eloquent etc)
assume your database should correspond to your models. This is simply not the
case; models contain business logic and may, may not or may in part refer to
database tables, NoSQL databases, flat files, an external API or whatever (the
"R" in ORM should really stand for "resource", not "relational"). The point is:
the models shouldn't care, and there should be no "conventional" mapping through
their names. (A common example would be a model of pages in multiple languages,
where the data might be stored in a page
table and a page_i18n
table for the
language-specific data.)
Also, the use of extensive and/or complicated config files sucks. (XML? This is 2020, people!)
Ornament's design goals are:
- make it super-simple to use vanilla PHP classes as models;
- promote the use of models as "dumb" data containers;
- encourage offloading of storage logic to helper classes ("repositories");
- make models extensible via an easy plugin mechanism.
Installation
$ composer require ornament/core
You'll likely also want auxiliary packages from the ornament/*
family.
Basic usage
Ornament models (or "entities" if you're used to Doctrine-speak) are really nothing more than vanilla PHP classes; there is no need to extend any base object of sorts (since you might want to do that in your own framework).
Ornament is a toolkit, so it supplies a number of Trait
s one can use
and
auxiliary decorator classes to extend your models' behaviour beyond the
ordinary.
The most basic implementation would look as follows:
<?php use Ornament\Core\Model; class MyModel { // The generic Model trait that bootstraps this class as an Ornament model; // it contains core functionality. use Model; /** * All protected properties on a model are considered read-only. */ protected int $id; /** * Public properties are read/write. To auto-decorate during setting, use * the `Model::set()` method. */ public string $name; /** * Private properties are just that: private. They're left alone. */ private string $password; } // Assuming $source is a handle to a data source (in this case, a PDO // statement): $model = MyModel::fromIterable($source->fetch(PDO::FETCH_ASSOC)); echo $model->id; // 1 echo $model->name; // Marijn echo $model->password; // Error: private property. $model->name = 'Linus'; // Ok; public property. $model->id = 2; // Error: read-only property.
PHP will take care of type coercion for builtins, while Ornament will handle more complex casing and decorating so you can also use classes as decorators (see below for more information).
The above example didn't do much yet except exposing the protected id
property
as read-only. Note however that Ornament models also prevent mutating undefined
properties; trying to set anything not explicitly set in the class definition
will throw an Error
mimicking PHP's internal error.
Annotating and decorating models
Ornament doesn't get really useful until you start decorating your models.
This is done (mostly) by specifying a type hint on a property with a class name
that implements Ornament\Core\DecoratorInterface
.
Getters for virtual properties
Ornament models support the concept of virtual properties (which are, by definition, read-only).
An example of a virtual property would be a model with a firstname
and
lastname
property, and a getter for fullname
. To mark a method as a getter,
attribute it with #[\Ornament\Core\Getter("property")]
:
<?php class MyModel { // ... #[\Ornament\Core\Getter("fullname")] protected function exampleGetter() : string { return $this->firstname.' '.$this->lastname; } }
The name and visibility of a getter (usually) don't matter; a best practice is
to mark them as protected
so they cannot be called from outside, and to give
them a reasonably descriptive name for your own sanity (in the above example,
getFullname
would have been better).
Decorator classes
As of version 0.16, Ornament supports three types of decorator classes: simple
backed enums, classes that work by just receiving the value, and decorators
implementing Ornament\Core\DecoratorInterface
. Generally, your decorators will
extend the Ornament\Core\Decorator
base class, but you can also use something
like Carbon\Carbon
.
First, an example using an enum:
<?php enum MyEnum : int { case cool = 1; case stuff = 2; } class MyModel { // ... public MyEnum $example; } // This now fails, since 3 is not in the enum: $model = MyModel::fromIterable(['example' => 3]);
If an enum decorator is marked as nullable, Ornament will use tryFrom
and the
above example would have not thrown an error, but instead have set
$model->example
to null
.
The first argument passed to the constructor is the raw value. If the decorator
extends Ornament's core Decorator class, the second argument is a
ReflectionProperty
of the property being decorated, which the custom decorator
can use to extract attributes for configuration. Finally, you may add multiple
Ornament\Core\Construct
attributes specifying additional arguments. In the
earlier example of Carbon, this could specify the time zone, for instance.
It is recommended that a decorating class also supports a __toString
method,
so one can seamlessly pass decorated properties back to a storage engine.
PHP, PDO and fetchObject
PDO's fetchObject
and related methods try to be clever by injecting
properties based on fetched database columns before the constructor is called.
PHP 7.4 doesn't like that, since a decorated property will be of the wrong type!
For this reason, it's now considered best practice to use PDO::FETCH_ASSOC
and
feeding the result through either Model::fromIterable
(for fetch
) or
Model::fromIterableCollection
(for fetchAll
).
E.g.:
<?php // ... return MyModel::fromIterable($stmt->fetch(PDO::FETCH_ASSOC));
Versions of Ornament <0.14 did not have this limitation as they specifically
worked with fetchObject
; this is no longer possible on PHP 7.4 so we strongly
recommend you upgrade to 0.15 or higher.
Custom object instantiation
Ornament supplies a constructor that expects key/value pairs of data to inject
into the model. Sometimes this is not what you want; maybe you're extending a
base class that expects each property to be specified as an argument to the
constructor (or whatever, e.g. Laravel's fill
method).
Default behaviour can be overridden using the initTransformer
static method,
passing a callback which takes the iterable $data
as its only argument and
must return the constructed object:
<?php MyModel::initTransformer(function (iterable $data) : MyModel { return new MyModel($data['id'], $data['password']); });
These transformers are on a per class basis. If you need it for all your
models, you should make them extend a base class and call initTransformer
on
that.
Loading and persisting models
This is your job. Wait, what? Yes, Ornament is storage engine agnostic. You may use an RDBMS, interface with a JSON API or store your stuff in Excel files for all we care. We believe that you shouldn't tie your models to your storage engine.
Our personal preference is to use "repositories" that handle this. Of course,
you're free to make a base class model for yourself which implements save()
or delete()
methods or whatever.
Stateful models
Having said that, you're not completely on your own. Models may use the
Ornament\Core\State
trait to expose some convenience methods:
isDirty()
: was the model changed since the last instantiation?isModified(string $property)
: specifically check if a property was modified.isPristine()
: the opposite ofisDirty
.markPristine()
: manually mark the model as pristine, e.g. after storing it. Basically this resets the initial state to the current state.
All these methods are public. You can use them in your storage logic to
determine how to proceed (e.g. skip an expensive UPDATE
operation if the model
isPristine()
anyway).
Preventing properties from being decorated
If your models are more than a "simple" data store, there might be properties on it you explicitly don't want decorated. Note that any private or static property is already left alone.
To explicitly tell Ornament to skip decoration for a public or protected
property, add the attribute Ornament\Core\NoDecoration
to it.