Objects
Version 0.2 ∙ 5 minutes read
Application data is made of objects. Use standard PHP class and attribute syntax to:
- define the structure of application objects,
- define how they are stored in the database,
- specify computation logic.
Status. This specification is a work in progress.
Contents:
- Object Types
- Properties
- Nullable Properties
- Default Values
- Table Columns
- Computed Properties
- Filtering And Sorting
- Search
- What's Missing In This Specification
- Implementation Status / Efforts Required
Object Types
Records
Top-level objects are stored in database records. For example, product objects are stored as records in products
table.
Top-level objects are instances of classes that extend from the Record
class.
Let’s call them just records.
For example, products are instances of Product
class:
/**
* @property int $qty Qty in stock
* @property float $price Regular price
* @property bool $enabled
*/
class Product extends Record
{
}
In addition to all properties defined in the class, every record has two standard properties:
id
- Unique auto-incremented record ID. It's used for creating relationships between objects, and for object selection in a grid.title
- Record title. It's used for in various places of the admin area, for example, while displaying the object in dropdown option list, or in the page title of an edit form.
Objects
Some objects don't have a designated table that stores them. Instead, they are stored in the parent object's record.
Let's call such child objects just objects.
For example, product images can be stored in the product record:
/**
* ...
* @property Image[] $images
*/
class Product extends Record
{
}
/**
* @property Product $parent
* @property string $path
* @property int $sort_order
*/
class Image extends Object_
{
}
Subtypes
Some records of the same table may have different structure.
For example, simple products and configurable products have many common properties that can be put into the base Product
class, but some other properties differ. A configurable product contains properties that a user should specify while ordering a product, and a list of the underlying simple products that will actually be shipped. Use #[Type]
attribute to define a record subtype:
/**
* @property string[] $config_properties
* @property Product[] $config_products
*/
#[Type('configurable')]
class Configurable extends Product
{
}
Properties
Objects store property values. For example, products may be of different color or size, have different quantity in stock. All of these are properties.
Properties are defined in the doc comment of the object's class using @property
tag:
/**
* @property int $qty Qty in stock
* @property float $price Regular price
* @property bool $enabled
*/
class Product extends Record
{
}
If you accidentally assign an undefined property a value, such assignment is ignored.
Each property definition has mandatory type (e.g. int
) and name (e.g. qty
). The property can be a scalar, an object, or an array.
Scalars
Scalar properties store regular, simple values.
To define a scalar property, use int
, string
, bool
, float
, \DateTime
(or Carbon
its better alternative) or mixed
property type:
/**
* @property string $name
* @property int $qty
* @property float $price
* @property bool $enabled
* @property Carbon $created_at
*/
class Product extends Record
{
}
In PHP, mixed
means "any type", while in Osm Admin, it means "any scalar type". In general, avoid mixed
properties, and use them only if absolutely necessary.
Objects
An object property contains an objects, or references a record.
To define an object property, use the class name of the referenced record (should extend Record
) or contained object (should extend Object_
), for example:
/**
* @property Product $parent
* ...
*/
class Image extends Object_
{
}
Arrays
An array property contains an array of scalars or objects.
To define an array property, use a scalar or object property type followed by []
:
/**
* ...
* @property Image[] $images
*/
class Product extends Record
{
}
Nullable Properties
Mark property as nullable using ?
syntax if some object may have no value for it. For example, a root product category has no parent category, hence it should be nullable:
/**
* @property string $name
* @property ?Category $parent
* @property Category[] $children
*/
class Category extends Record
{
}
User has to provide values for all non-nullable properties, so, nullable properties make object creation easier. In fact, consider marking all properties nullable, unless a value is really, really required.
Default Values
Another way to simplify object creation is providing sensible default values using #[Default_]
attribute. Let’s refine the definition of the product class:
/**
* @property ?string $name
* @property int $qty #[Default_(0)]
* @property float $price #[Default_(0.0)]
* @property bool $enabled #[Default_(true)]
*/
class Product extends Record
{
}
Table Columns
Records of the same class are stored in one table. For example, all product objects are stored as records in the products
table.
The table name is inferred from the short class name, Product
. You can specify a custom table name using the #[Table]
attribute:
/**
* ...
*/
#[Table('my_products')]
class Product extends Record
{
}
By default, a table has two columns:
id
is unique auto-incremented unsigned integerdata
JSON column stored all the properties. Null values are not stored.
For example:
products
id data
-------------------------------------------
1 {"name": "Pink Bag", "qty": 5}
2 {"name": "Blue Dress", "qty": 10}
3 {"name": "Black Jacket", "qty": 20}
You may add #[Explicit]
attribute to a property definition to create an explicit table column:
/**
* @property ?string $name #[Explicit]
* ...
*/
class Product extends Record
{
}
The underlying table changes as follows:
products
id name data
-----------------------------------
1 Pink Bag {"qty": 5}
2 Blue Dress {"qty": 10}
3 Black Jacket {"qty": 20}
Column type used, and other database schema details are dependent on the property type and additional attributes. Nullable explicit properties make nullable table columns.
string
By default, an explicit string
property is stored as TEXT
. You can force it to be VARCHAR
using #[Length]
attribute specifying a value that is small enough for it:
/**
* @property ?string $name #[Explicit, Length(255)]
* ...
*/
class Product extends Record
{
}
int
By default, an explicit int
property is stored as signed INT
. Change that using #[Tiny]
, [Small]
, #[Long]
and #[Unsigned]
attributes:
/**
* @property int $qty #[Default_(0), Explicit, Unsigned, Long]
*/
class Product extends Record
{
}
float
By default, an explicit float
property is stored as DECIMAL(18, 2)
. Change that using #[Precision]
and [Scale]
attributes:
/**
* @property float $price #[Default_(0.0), Explicit, Scale(4)]
*/
class Product extends Record
{
}
Carbon
(or \DateTime
)
These columns are stored as DATETIME
columns, in UTC timezone.
Objects
Non-record explicit objects are stored in a JSON column.
For record objects, foreign key columns are explicitly created. For example:
/**
* @property ?Category $parent #[Explicit]
* ...
*/
class Category extends Record
{
}
This property creates parent_id UNSIGNED INT
column, an index and a foreign key constraint. By default, the constraint rule has a ON DELETE CASCASE
clause, you may use #[OnDeleteRestrict]
or [OnDeleteSetNull]
to change that.
Computed Properties
You may have a property computed based on a SQL-like formula:
/**
* @property ?Category $parent #[Explicit]
* @property ?int $level #[Computed("(parent.level ?? -1) + 1")]
* @property ?string $id_path #[
* Computed("(parent.id_path IS NOT NULL " .
* "? parent.id_path + '/' : '') + id")
* ]
* ...
*/
class Category extends Record
{
}
Computed values are stored in the database, and updated as needed.
Actually there are two more variations of computed properties: virtual properties and overridable properties.
Virtual Properties
Rather than storing computed value in the database, you may compute it whenever it's needed. In this case, use #[Virtual]
attribute:
/**
* @property ?Category $parent #[Explicit]
* @property bool $root #[Virtual("parent.id IS NULL")]
* @property bool $top #[Virtual("parent.parent.id IS NULL")]
* ...
*/
class Category extends Record
{
}
Overridable Properties
Finally, you may allow user to override the computed value using the #[Overridable]
attribute:
/**
* @property ?Category $parent #[Explicit]
* @property string $name #[Overridable("parent.name")]
* ...
*/
class Category extends Record
{
}
Filtering And Sorting
Properties are indexed for optimal performance.
By default, a property is not indexed. In general, keep properties not indexed unless it's really needed.
You can index it manually for filtering and sorting purposes using #[Filterable]
and #[Sortable]
attributes, accordingly:
/**
* @property int $qty #[Filterable, Sortable]
* @property float $price #[Filterable, Sortable]
* @property bool $enabled #[Filterable, Sortable]
*/
class Product extends Record
{
}
If a property is shown in a grid, it's implicitly filterable and sortable, except virtual properties. If a grid shows a formula rather than a property, it's also neither filterable nor sortable.
If a property is filterable or sortable, it also has an explicit column in the underlying table.
You may more make properties filterable only in the specific application area using #[Filterable(Api::class)]
syntax.
Search
Properties can also be indexed for full-text search.
By default, all records are searchable by title
property. Add an attribute to a search index using the #[Searchable]
attribute:
/**
* @property string $description #[Searchable]
*/
class Product extends Record
{
}
What's Missing In This Specification
- implementation status is outdated
Implementation Status / Efforts Required
Currently, Osm Admin doesn't validate property definitions and applied attributes.
Although you can define any property in a data class, currently only int
and string
properties are supported.
Arrays are especially tricky, be it an array of scalars, an array of objects, or an array of record references. How they should be stored, queried, displayed?
Upgrading a database is a non-trivial task. What if property type changes? What if property is renamed? Deleted? What if nullability changes? How the data is preserved? How computed properties are preserved? This topic is so wide that it requires a separate specification.
Computed properties are well, not computed.
#[Filterable]
and #[Sortable]
attributes are ignored. #[Searchable]
attribute is also ignored.
R. Validate property definitions and applied attributes.
R. Support all the rest property types.
R. Support arrays.
R. Test both implicit and explicit property versions.
R. Specify how database upgrades work.
R. Implement computed properties.
R. Handle #[Filterable]
and #[Sortable]
attributes.
R. Handle #[Searchable]
attribute.