beecubu / php-foundation-core
Main package of PHP Object Foundation Framework.
Requires
- php: >=7.1.0
- ext-json: *
- ext-openssl: *
Requires (Dev)
- phpunit/phpunit: ^8
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-master
- v3.10.0.2
- v3.10.0.1
- v3.10.0
- v3.9.0
- v3.8.0
- v3.7.0.1
- v3.7.0
- v3.6.2.3
- v3.6.2.2
- v3.6.2.1
- v3.6.2
- 3.6.1.1
- v3.6.1
- v3.6.0
- v3.5.0
- v3.4.1
- v3.4.0
- v3.3.0.1
- v3.3.0
- v3.2.0
- v3.1.0
- v3.0.0
- v2.9.0
- v2.8.0.4
- v2.8.0.3
- v2.8.0.2
- v2.8.0.1
- v2.8.0
- v2.7.3
- v2.7.2
- v2.7.1
- v2.7.0
- v2.6.4.1
- v2.6.4
- v2.6.3
- v2.6.2.2
- v2.6.2.1
- v2.6.2
- v2.6.1
- v2.6.0
- v2.5.1
- v2.5.0.1
- v2.5.0
- v2.4.1
- v2.3.2
- v2.3.1
- v2.3.0.1
- v2.3.0
- v2.2.3
- v2.2.2.1
- v2.2.2
- v2.2.1
- v2.2.0
- v2.1.0
- v2.0.0.3
- v2.0.0.2
- v2.0.0.1
- v2.0.0
This package is auto-updated.
Last update: 2026-10-14 16:38:47 UTC
README
Core package for PHP Object Foundation. This is the base used by the rest of the framework libraries. It provides:
- A strict property system (
Objectum) with typed properties and validation. - Serialization and JSON export/import (
Serializable) with custom hooks. - Enum utilities (
Enum), property definitions (Property), and serialization helpers. - Common tools: date/time utilities and formatters.
Requirements
- PHP 7.1+
ext-opensslext-json
Installation
composer require beecubu/php-foundation-core
Tests
After installing the package's development dependencies, run from this directory:
php -d date.timezone=Europe/Madrid ./vendor/bin/phpunit --bootstrap vendor/autoload.php --test-suffix Tests.php Tests
The date formatter tests assume Europe/Madrid as the server timezone when converting
to America/Mexico_City. Set it explicitly to make their results independent of the
local PHP configuration.
Key Concepts
Objectum (Typed Properties)
Define properties with types and access rules:
<?php
use Beecubu\Foundation\Core\Objectum;
use Beecubu\Foundation\Core\Property;
/**
* User data with validated properties.
*
* @property string|null $name User name, or null before assignment.
* @property int|null $age User age, or null before assignment.
* @property bool|null $active Whether the user is active, or null before assignment.
* @method bool hasName()
*/
class User extends Objectum
{
protected function properties(): void
{
parent::properties();
$this->properties += [
'name' => [Property::READ_WRITE, Property::IS_STRING],
'age' => [Property::READ_WRITE, Property::IS_INTEGER],
'active' => [Property::READ_WRITE, Property::IS_BOOLEAN],
];
}
}
$u = new User();
$u->name = 'Ada';
$u->active = 'yes';
Inheritance and static analysis
Call parent::properties() before appending definitions with $this->properties += [...].
PHP's array union keeps existing keys: a child definition with the same name does not
replace the parent's definition.
Property::* declarations provide runtime validation. PHPDoc gives PhpStorm, Psalm,
and other analysis tools the types of these dynamic properties. Use @property for
READ_WRITE, @property-read for READ_ONLY, and @property-write for WRITE_ONLY.
Document dynamic methods that your model uses with @method, such as hasName().
For arrays, hasItems() checks whether the array contains elements; for other readable
properties, hasXxx() checks whether the value is non-null.
Core's internal property and serialization configuration arrays are also annotated so subclasses can extend them without treating the inherited fields as unknown types. These annotations do not change runtime validation or require PHP typed properties.
Arrays of objects
The third item in an IS_ARRAY definition can name the accepted object class:
$this->properties += [
'members' => [Property::READ_WRITE, Property::IS_ARRAY, User::class],
];
Document this property as @property User[] $members; the default getter returns an
empty array when no value is stored. Class metadata is used by the array insertion
helper and serialization. In Objectum, assigning an entire IS_ARRAY value checks
that it is an array but does not validate each element's class.
An annotation such as string[] describes the expected values to analysis tools;
it does not add runtime validation of those values.
Enums
use Beecubu\Foundation\Core\Enum;
class Status extends Enum
{
public const Draft = 'draft';
public const Sent = 'sent';
}
Use enums in properties with validation:
use Beecubu\Foundation\Core\Property;
$this->properties += [
'status' => [Property::READ_WRITE, Property::IS_ENUM, Status::class],
];
Serializable
Serializable extends Objectum and adds JSON/RawData serialization with custom hooks and property filtering.
<?php
use Beecubu\Foundation\Core\Serializable;
use Beecubu\Foundation\Core\Property;
class Article extends Serializable
{
protected function properties(): void
{
parent::properties();
$this->properties += [
'title' => [Property::READ_WRITE, Property::IS_STRING],
'content' => [Property::READ_WRITE, Property::IS_STRING],
];
}
}
$a = new Article();
$a->title = 'Hello';
$json = $a->json(); // stdClass
$raw = $a->rawData(); // stdClass
$copy = Article::instanceWithJson($json);
Encrypted Strings
If a property is Property::IS_STRING_ENCRYPTED, values are stored encrypted in raw data.
In JSON, the framework exports them as an EncryptedStringProperty object with kind, encoding, and value.
$this->properties += [
'secret' => [Property::READ_WRITE, Property::IS_STRING_ENCRYPTED],
];
Example JSON shape:
$json = $article->json();
$json->secret->kind; // encrypted-string
$json->secret->encoding; // base64
$json->secret->value; // base64 payload
When importing from JSON:
- If
secretis astring, it is treated as plain text and encrypted. - If
secretis an object, it is treated as anEncryptedStringProperty.
Object Auto-Initialization
By default, IS_OBJECT properties are auto-created when first accessed (if no value is set).
You can disable this globally:
Beecubu\Foundation\Core\Objectum::$CreateObjectPropertiesOnGet = false;
IS_OBJECT and IS_OBJECT_NULLABLE can also accept more than one class by passing an array of class names as the third property definition value:
$this->properties += [
'target' => [Property::READ_WRITE, Property::IS_OBJECT_NULLABLE, [User::class, Team::class]],
];
When importing serialized data for a property with multiple possible classes, include _class in the raw/json object so the framework can resolve which concrete class must be created.
On-Demand References
SerializableProperty::CREATE_ON_DEMAND_BY_ID allows lazy-loading related objects by id.
Important: the referenced class must implement protected static function instanceByRawDataId($id) or lazy loading will fail.
This method is called by the serialization layer when a property is accessed and only an id is available. The default implementation throws an exception, so each on-demand class must provide its own factory.
use Beecubu\Foundation\Core\SerializableProperty;
$this->properties += [
'author' => [Property::READ_WRITE, Property::IS_OBJECT_NULLABLE, Author::class, SerializableProperty::CREATE_ON_DEMAND_BY_ID],
];
Example implementation:
class Author extends Serializable
{
// properties()...
protected static function instanceByRawDataId($id)
{
// Fetch from persistence layer by id and return an Author instance.
// Example: return AuthorRepository::findById($id);
}
}
Serializable Snapshots
SerializableProperty::IS_SNAPSHOT stores a read-only partial copy of a related Serializable object instead of storing the live object or only its id. This is useful for historical data where the owner must keep a small immutable picture of another entity.
The source class declares which fields are part of the snapshot:
class Status extends Serializable
{
protected function properties(): void
{
parent::properties();
$this->properties += [
'title' => [Property::READ_WRITE, Property::IS_STRING],
'color' => [Property::READ_WRITE, Property::IS_STRING],
'description' => [Property::READ_WRITE, Property::IS_STRING],
];
}
protected function snapshotProperties(): void
{
$this->snapshotProperties = ['title', 'color'];
}
}
The owner marks the property as a snapshot:
use Beecubu\Foundation\Core\SerializableProperty;
$this->properties += [
'status' => [Property::READ_WRITE, Property::IS_OBJECT_NULLABLE, Status::class, SerializableProperty::IS_SNAPSHOT],
];
IS_SNAPSHOT is only supported for IS_OBJECT, IS_OBJECT_NULLABLE, and typed IS_ARRAY object properties. Using it with another property type throws SerializableUnsupportedSnapshotPropertyTypeException.
Assigning a Status creates a SerializableSnapshot. The snapshot only exposes the configured properties and all of them are read-only:
$notification->status = $status;
$notification->status->isSnapshot(); // true
$notification->status->isSnapshotOf(Status::class); // true
$notification->status->title; // copied from $status
If an existing SerializableSnapshot is assigned directly, the framework validates that its source class is accepted by the property and that its scope matches the property scope when one is defined.
Snapshots can also be scoped when the same source class needs different partial views:
class Status extends Serializable
{
protected function snapshotProperties(): void
{
$this->snapshotProperties = [
'notification' => ['title', 'color'],
'badge' => ['color'],
];
}
}
$this->properties += [
'status' => [Property::READ_WRITE, Property::IS_OBJECT_NULLABLE, Status::class, SerializableProperty::IS_SNAPSHOT, 'notification'],
];
For dynamic cases, override snapshotPropertiesForScope(?string $scope = null): array and return the property names for that scope.
In RawData mode, snapshots always include _class and include _scope when a scope is defined. These metadata fields are used to reconstruct the virtual snapshot and to validate consistency. If a property defines a scope and imported RawData contains a different _scope, the framework throws SerializableSnapshotScopeMismatchException.
IS_SNAPSHOT also supports IS_OBJECT / IS_OBJECT_NULLABLE properties with multiple accepted classes:
$this->properties += [
'state' => [Property::READ_WRITE, Property::IS_OBJECT_NULLABLE, [OpenState::class, ClosedState::class], SerializableProperty::IS_SNAPSHOT],
];
It can also be used with typed arrays of objects:
$this->properties += [
'states' => [Property::READ_WRITE, Property::IS_ARRAY, State::class, SerializableProperty::IS_SNAPSHOT],
];
SerializableProperty::CREATE_SNAPSHOT_BY_ID behaves like IS_SNAPSHOT, but also accepts reference data as input. When an id or reference payload is assigned or imported, the framework normalizes it through TargetClass::parseDataBeforeInsertInIvarsOnDemand($data), calls TargetClass::instanceByRawDataId($id) immediately, and stores the resulting snapshot.
$this->properties += [
'status' => [Property::READ_WRITE, Property::IS_OBJECT_NULLABLE, Status::class, SerializableProperty::CREATE_SNAPSHOT_BY_ID],
'states' => [Property::READ_WRITE, Property::IS_ARRAY, State::class, SerializableProperty::CREATE_SNAPSHOT_BY_ID],
];
Important: the target class must implement protected static function instanceByRawDataId($id). If the incoming data is not a primitive id, use parseDataBeforeInsertInIvarsOnDemand() in the target class to extract the id exactly as with CREATE_ON_DEMAND_BY_ID. The property value is still a SerializableSnapshot (or an array of snapshots), never a lazy reference.
Tools
Helpers
use Beecubu\Foundation\Core\Tools\Helpers;
Helpers::empty($value); // Better empty() for framework objects
Formatters
use Beecubu\Foundation\Core\Tools\Formatters\NumberFormatter;
use Beecubu\Foundation\Core\Tools\Formatters\DateFormatter;
use Beecubu\Foundation\Core\Tools\Formatters\StringFormatter;
use Beecubu\Foundation\Core\Tools\Formatters\PluralsFormatter;
NumberFormatter::numberToString(1234.56, 'en'); // "1,234.56"
DateFormatter::dateToString(new DateTime(), 'en');
StringFormatter::utf82iso('àéí');
PluralsFormatter::plural('day', 2, 'en'); // "days"
DateTime Helper
use Beecubu\Foundation\Core\Tools\DateTime;
$dt = new DateTime('@1717352123'); // respects php.ini timezone
Custom Properties (Serialization Hooks)
Serializable lets you override how specific properties are imported/exported by defining custom flags in customProperties().
The flags are defined in Beecubu\Foundation\Core\CustomProperty and control:
- When the hook runs: import, export, or both
- Which mode: JSON, RawData, or both
Typical pattern:
use Beecubu\Foundation\Core\CustomProperty;
class Report extends Serializable
{
protected function properties(): void
{
parent::properties();
$this->properties += [
'status' => [Property::READ_WRITE, Property::IS_STRING],
'secret' => [Property::READ_WRITE, Property::IS_STRING],
];
}
protected function customProperties(): void
{
$this->customProperties += [
// Only transform when exporting to JSON
'status' => CustomProperty::ON_EXPORT | CustomProperty::ON_JSON_MODE,
// Only transform when importing from RawData
'secret' => CustomProperty::ON_IMPORT | CustomProperty::ON_RAW_MODE,
];
}
protected function exportCustomProperty(string $property, $value, bool $rawDataMode, ?array $fields = null, bool $excluding = true, bool $ignoreEmptyValues = true)
{
if ($property === 'status') return strtoupper($value);
return $value;
}
protected function importCustomProperty(string $property, $value, bool $replace = true, bool $rawDataMode = false, bool $writableOnly = false)
{
if ($property === 'secret') return trim($value);
return $value;
}
}
Useful flag presets:
CustomProperty::ON_IMPORT_AND_EXPORT_IN_ALL_MODESCustomProperty::ON_IMPORT | CustomProperty::ON_JSON_MODECustomProperty::ON_EXPORT | CustomProperty::ON_RAW_MODE
Property Types Reference
Common property types:
Property::IS_STRINGProperty::IS_STRING_ENCRYPTEDProperty::IS_INTEGERProperty::IS_FLOATProperty::IS_BOOLEANProperty::IS_DATEProperty::IS_ENUMProperty::IS_ENUM_NULLABLEProperty::IS_ARRAYProperty::IS_ARRAY_ENUMProperty::IS_OBJECTProperty::IS_OBJECT_NULLABLEProperty::IS_DICTIONARYProperty::IS_BINARY
Access types:
Property::READ_ONLYProperty::READ_WRITEProperty::WRITE_ONLY