beecubu/php-foundation-core

Main package of PHP Object Foundation Framework.

Maintainers

Package info

bitbucket.org/beecubu/php-foundation-core

pkg:composer/beecubu/php-foundation-core

Transparency log

Statistics

Installs: 278

Dependents: 7

Suggesters: 0

v3.10.0.1 2026-06-24 21: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

Key Concepts

Objectum (Typed Properties)

Define properties with types and access rules:

<?php

use Beecubu\Foundation\Core\Objectum;
use Beecubu\Foundation\Core\Property;

class User extends Objectum
{
    protected function properties(): void
    {
        $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';

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