Search by

sugatasei / bredala-data

sugatasei

PHP modeling and data manipulation class

Package info

github.com/sugatasei/bredala-data

pkg:composer/sugatasei/bredala-data

Statistics

Installs: 11

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

6.1.1 2026-09-23 11:30 UTC

This package is auto-updated.

Last update: 2026-11-04 13:40:35 UTC


README

Outils de modélisation et de manipulation de données pour PHP.

Pas d'ORM, pas de constructeur de requètes, pas de persistance, pas de validation, pas de relations : uniquement un modèle abstrait avec conversion tableau/JSON, un contrat d'adaptateur vers un stockage, un micro-cache de durée de vie du script, et un lecteur de métadonnées de classe.

Installation

composer require sugatasei/bredala-data

Claude Code

Ce package fournit un skill Claude Code dans skills/bredala-data/ qui documente les patterns d'usage et les pièges de la librairie (import() qui ignore silencieusement les clés inconnues, export() qui passe par un aller-retour JSON, mémoïsation non invalidée de Reflection, etc.).

Dans un projet qui dépend de sugatasei/bredala-data, copie-le une fois dans .claude/skills/ après composer install pour que Claude Code le charge automatiquement (le nom du dossier doit correspondre au name déclaré dans SKILL.md) :

cp -r vendor/sugatasei/bredala-data/skills/bredala-data .claude/skills/bredala-data

ModelAbstract

Bredala\Data\ModelAbstract Classe de base pour créer des entités, des modèles, des DTO.

Elle implémente Bredala\Data\ModelInterface, qui étend lui-même JsonSerializable. La surface sérialisée d'un modèle est exactement l'ensemble de ses propriétés publiques, non statiques et déclarées. Les propriétés protected, private et static en sont exclues : c'est le mécanisme pour garder des données internes hors du payload.

use Bredala\Data\ModelAbstract;

class User extends ModelAbstract
{
    public ?int $id = null;
    public string $email = '';
    public string $name = '';
}

Chaque propriété publique doit avoir une valeur par défaut : la sérialisation les lit toutes, et une propriété typée non initialisée lève une Error.

Construction

  • __construct(array $data = []) Appelle init() puis import($data).
  • create(array $data = []) Équivalent statique de new static($data).

Initialisation

  • init(): static Point d'extension pour les valeurs par défaut qu'une valeur littérale ne peut pas exprimer. Ne fait rien dans la classe de base.

init() s'exécute avant import() : une donnée entrante écrase donc toujours ce qu'init() a posé, et ce qu'init() a posé survit quand la clé est absente.

class Post extends ModelAbstract
{
    public string $slug = '';
    public string $createdAt = '';

    public function init(): static
    {
        $this->createdAt = date('Y-m-d H:i:s');
        return $this;
    }
}

Import

  • import(array $data = []): static Affecte chaque clé du tableau à la propriété de même nom.

import() filtre les clés reçues contre properties(), c'est-à-dire les propriétés publiques non statiques déclarées. Tout le reste — clé inconnue, propriété protected, private ou static — est silencieusement ignoré : aucune propriété dynamique, aucune dépréciation, aucune exception. Une faute de frappe dans une clé passe donc inaperçue ; le champ est simplement absent en aval.

Pour alimenter une propriété non publique, surcharger import() et faire le mappage à la main.

La liste blanche porte sur la visibilité, pas sur la sensibilité : id, role et consorts restent écrivables depuis les données brutes dès lors qu'ils sont publics. Un type incorrect lève une TypeError.

Pour des données non maîtrisées (corps de requète, API externe), restreindre en amont aux champs réellement modifiables — properties() ne suffit pas ici, puisque c'est déjà le filtre appliqué par import() :

$editable = ['email', 'name'];  // ni id, ni role
$user->import(array_intersect_key($input, array_flip($editable)));

Export

  • export(): array Retourne un tableau de scalaires. Implémenté par un aller-retour json_encode / json_decode.
  • jsonSerialize(): mixed Retourne les propriétés telles quelles.
  • properties(): array Retourne les noms des propriétés sérialisées, dans l'ordre de déclaration (propriétés propres avant propriétés héritées).

Les deux exports ne donnent pas la même forme : export() convertit récursivement les modèles imbriqués en tableaux, alors que jsonSerialize() conserve les objets.

$user = User::create(['email' => 'a@b.c', 'name' => 'Tom']);

$user->export();        // ['id' => null, 'email' => 'a@b.c', 'name' => 'Tom']
json_encode($user);     // {"id":null,"email":"a@b.c","name":"Tom"}

export() hérite des limites de json_encode : une ressource, une closure, NAN/INF ou une référence récursive font échouer l'encodage, et le type de retour : array transforme le résultat en TypeError portant sur la signature d'export() — pas en JsonException nommant la propriété fautive.

AdapterAbstract

Bredala\Data\AdapterAbstract Conversion d'un modèle vers un stockage et inversement. Implémente Bredala\Data\AdapterInterface.

  • encode(mixed $input): mixed Encode un modèle pour le stockage. Identité par défaut.
  • decode(mixed $input): mixed Décode une représentation de stockage. Identité par défaut.
  • unique(array $rows, string $property): array Retourne les valeurs distinctes d'une colonne, réindexées.

Les deux conversions sont l'identité par défaut, pas abstraites : une sous-classe qui oublie d'en redéfinir une laisse passer les données telles quelles sans erreur.

use Bredala\Data\AdapterAbstract;

class UserAdapter extends AdapterAbstract
{
    public function encode(mixed $input): mixed
    {
        return ['id' => $input->id, 'email' => $input->email, 'full_name' => $input->name];
    }

    public function decode(mixed $input): mixed
    {
        return User::create([
            'id' => $input['id'] ?? null,
            'email' => $input['email'] ?? '',
            'name' => $input['full_name'] ?? '',
        ]);
    }
}

unique() sert à collecter les clés étrangères distinctes d'un jeu de résultats pour alimenter une seule requète IN (…) :

$authorIds = UserAdapter::unique($postRows, 'author_id');

Elle fonctionne sur des tableaux de tableaux comme sur des tableaux d'objets (propriétés publiques). Le second argument est optionnel : omis, unique() déduplique directement le tableau reçu. Les lignes dépourvues de la colonne sont ignorées silencieusement. null, '' et [] sont écartés. La comparaison passe par array_unique() en SORT_REGULAR, donc en == : 1 et '1' fusionnent toujours, comme 0 et false. C'est le comportement voulu pour une colonne d'identifiants ; pour une déduplication stricte, comparez vous-même en ===.

Reflection

Bredala\Data\Reflection Lecture mémoïsée des métadonnées d'une classe. C'est ce qu'utilise ModelAbstract::properties().

  • getInstance(string $classname): static Retourne le lecteur mémoïsé pour cette classe. Le constructeur est protected : c'est le seul point d'entrée.
  • properties(): array Retourne les noms des propriétés publiques non statiques.
  • hasProperty(string $key): bool
  • methods(): array Retourne les noms des méthodes publiques non statiques.
  • hasMethod(string $key): bool

Les instances sont mises en cache par nom de classe, sans invalidation. La classe est inspectée, jamais une instance : une propriété ajoutée dynamiquement n'est jamais listée. Un nom de classe inconnu lève une ReflectionException dès getInstance(), et n'est pas mis en cache. Les interfaces sont acceptées.

methods() inclut les méthodes publiques héritées : sur une sous-classe de ModelAbstract, import, export, init, properties, jsonSerialize et __construct apparaissent à côté des méthodes propres.

Utilisation

use Bredala\Data\AdapterAbstract;
use Bredala\Data\ModelAbstract;

class User extends ModelAbstract
{
    public ?int $id = null;
    public string $email = '';
    public string $name = '';

    public function init(): static
    {
        $this->name = 'anonyme';
        return $this;
    }
}

class UserAdapter extends AdapterAbstract
{
    public function decode(mixed $input): mixed
    {
        return User::create([
            'id' => (int) $input['id'],
            'email' => $input['email'],
            'name' => $input['full_name'],
        ]);
    }
}

$adapter = new UserAdapter();

$rows = [
    ['id' => '1', 'email' => 'tom@example.test', 'full_name' => 'Tom'],
    ['id' => '2', 'email' => 'tim@example.test', 'full_name' => 'Tim'],
];

$users = array_map(fn(array $row) => $adapter->decode($row), $rows);

foreach ($users as $user) {
    print_r($user->export());
}

Tests

composer install
vendor/bin/phpunit