Search by

beecubu / php-foundation-core

Beecubu

Main package of PHP Object Foundation Framework.

Package info

bitbucket.org/beecubu/php-foundation-core

pkg:composer/beecubu/php-foundation-core

Statistics

Installs: 297

Dependents: 7

Suggesters: 0

v3.10.0.2 2026-10-14 16:37 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-openssl
  • ext-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 secret is a string, it is treated as plain text and encrypted.
  • If secret is an object, it is treated as an EncryptedStringProperty.

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_MODES
  • CustomProperty::ON_IMPORT | CustomProperty::ON_JSON_MODE
  • CustomProperty::ON_EXPORT | CustomProperty::ON_RAW_MODE

Property Types Reference

Common property types:

  • Property::IS_STRING
  • Property::IS_STRING_ENCRYPTED
  • Property::IS_INTEGER
  • Property::IS_FLOAT
  • Property::IS_BOOLEAN
  • Property::IS_DATE
  • Property::IS_ENUM
  • Property::IS_ENUM_NULLABLE
  • Property::IS_ARRAY
  • Property::IS_ARRAY_ENUM
  • Property::IS_OBJECT
  • Property::IS_OBJECT_NULLABLE
  • Property::IS_DICTIONARY
  • Property::IS_BINARY

Access types:

  • Property::READ_ONLY
  • Property::READ_WRITE
  • Property::WRITE_ONLY