ccmbenchmark / ting
Ting : a lightweight datamapper
Requires
- php: >=8.1
- aura/sqlquery: ^2.6 || ^3.0
- doctrine/cache: ^1.6
- symfony/property-access: ^6.0 || ^7.0 || ^8.0
Requires (Dev)
- ext-mysqli: *
- ext-pgsql: *
- atoum/atoum: ^4.2
- atoum/stubs: ^2.0
- brick/geo: ^0.5.1
- pimple/pimple: ^3.0
- symfony/cache: ^6.0 || ^7.0 || ^8.0
- symfony/uid: ^6.0 || ^7.0 || ^8.0
Suggests
- brick/geo: Allow support of MariaDB Geometry type
- pimple/pimple: Service container.
- symfony/uid: To work with Uuid.
Provides
None
Conflicts
None
Replaces
None
- dev-master
- 4.x-dev
- 4.1.1
- 4.1.0
- 4.0.1
- 4.0.0
- 4.0.0-rc.1
- 3.13.1
- 3.13.0
- 3.12.1
- 3.12.0
- 3.11.0
- 3.10.6
- 3.10.5
- 3.10.4
- 3.10.3
- 3.10.2
- 3.10.1
- 3.10.0
- 3.9.1
- 3.9.0
- 3.8.1
- 3.8.0
- 3.7.3
- 3.7.2
- 3.7.1
- 3.7.0
- 3.6.2
- 3.6.1
- 3.6.0
- 3.5.8
- 3.5.7
- 3.5.6
- 3.5.5
- 3.5.4
- 3.5.3
- 3.5.2
- 3.5.1
- 3.5.0
- 3.5.0-beta.2
- 3.5.0-beta
- 3.4.x-dev
- 3.4.4
- 3.4.3
- 3.4.2
- 3.4.1
- 3.4.0
- 3.3.0
- 3.2.1
- 3.2.0
- 3.1.1
- 3.1.0
- 3.0.0
- 2.7.0
- 2.6.2
- 2.6.1
- 2.6.0
- 2.5.1
- 2.5.0
- 2.4.0
- 2.3.0
- 2.2.0
- 2.1.0
- 2.0.0
- 1.1.x-dev
- 1.1.5
- 1.1.4
- 1.1.3
- 1.1.2
- 1.1.1
- 1.1.0
- 1.0.0
- 1.0-beta
- 1.0-alpha
- dev-feature/remove-construct-on-interfaces
- dev-feature/dx-querybuilder
- dev-feature/docs-v4
- dev-chore/typehint-3
- dev-feature/value-object-hydrator
- dev-chore/typehint-2
- dev-chore/typehint
- dev-fix/v4-post-merge-v3
- dev-feature/merge-getByCriteriaWithOrderAndLimit-getByCriteria
- dev-feature/support-php-81
- dev-feature/support-natives-pgsql-classes-drop-php-80
- dev-feature/phpstan
- dev-feature/vo-hydrator-driver-fetch-object
- dev-feature/vo-hydrator-simple
- dev-feature/vo-hydrator
- dev-fix/avoid-iterator-notice
- dev-fix/metadatainitializer-generics
- dev-chore/bump-aura-sqlquery
- dev-feature/serialization-without-listener
- dev-feature/debuginfo
- dev-fix/add-exception-when-pimple-is-missing
- dev-feature/serialization-interface
- dev-feature/remove-metadata-setter-getter
- dev-chore/remove-uuid-generation
- dev-fix/remove-dynamic-property
- dev-feature/remove-deprecated
- dev-chore/add-phpstan
- dev-feature/use-weakmap
- dev-feature/datetimeimmutable-serialization
- dev-fix/mysqli-driver-ping-exception
- dev-feature/weakmap
- dev-feature/typehint
- dev-feature/serialize-datetimeimmutable
- dev-chore/setup-phpstan
- dev-fix/entity-identification-on-hydratation
- dev-fix/tests-8.1
- dev-feature/use_root_ns_for_func_calls
- dev-fix/pgsql-distinct
- dev-ci/php81
- dev-update_changelog
- dev-feature/add-query-order-limit
- dev-fix/ch/fix-mysql-driver
- dev-feature/ch/CCM-3208-add-timezone-on-database-connection
- dev-feature/ch/CCM-2809-allow-array-in-mysql
- dev-feature/jv/CCM-1458-corrections-des-retour-derreur-1
- dev-test/gitlab
- dev-feature/issue-11-pimple-suggest
- dev-feature/test-continuousphp
- dev-feature/FWPHP-243-shippable
This package is auto-updated.
Last update: 2026-10-07 10:08:24 UTC
README
Ting est une implémentation simple du pattern DataMapper pour PHP. Il fonctionne avec MySQL et PostgreSQL, sous licence Apache-2.0.
Il a quelques particularités et choix de conception distinctifs :
- Implémentation 100% PHP (pas de PDO, pas de XML)
- Pas de couche d'abstraction : vous parlez directement le langage de votre SGBD
- Rapide, faible consommation mémoire
- Simple à utiliser, simple à étendre
Vous pouvez lire ces quelques exemples, ou consulter plus d'exemples.
Si vous utilisez Symfony, voir ting_bundle pour la couche d'intégration (configuration YAML/attributs, Value Resolver, User Provider, etc.).
Sommaire
- Bootstrap
- Déclarer une entité
- Récupérer un objet par ID
- Requête simple
- Requête préparée
- QueryBuilder
- Écriture : l'UnitOfWork
- Hydrators
- Serializers
- Drivers
Bootstrap
Tout part de CCMBenchmark\Ting\Services, un petit conteneur Pimple qui assemble ConnectionPool, MetadataRepository, UnitOfWork, RepositoryFactory, etc.
<?php $services = new \CCMBenchmark\Ting\Services(); // Charge tous les fichiers *Repository.php correspondant au motif et enregistre leur Metadata $services ->get('MetadataRepository') ->batchLoadMetadata('sample\src\model', __DIR__ . '/model/*Repository.php'); // Configuration des connexions $connections = [ 'main' => [ 'namespace' => '\CCMBenchmark\Ting\Driver\Mysqli', // ou \CCMBenchmark\Ting\Driver\Pgsql 'master' => [ 'host' => '127.0.0.1', 'user' => 'root', 'password' => 'p455w0rd', 'port' => 3306, ], 'slaves' => [ // optionnel, un slave est choisi au hasard et réutilisé pour la durée de vie du pool ['host' => '127.0.0.1', 'user' => 'root', 'password' => 'p455w0rd', 'port' => 3306], ], ], ]; $services->get('ConnectionPool')->setConfig($connections); // Optionnel : options par base de données (ex: timezone) $services->get('ConnectionPool')->setDatabaseOptions([ 'world' => ['timezone' => 'UTC+1'], ]); $cityRepository = $services->get('RepositoryFactory')->get('\sample\src\model\CityRepository');
pimple/pimple est requis pour Services (composer require pimple/pimple) ; ting ne fait que le suggérer, pour rester optionnel si vous assemblez les composants vous-même.
Déclarer une entité
Une entité est une simple classe PHP. Chaque propriété accessible en lecture/écriture qui doit être suivie par l'UnitOfWork (voir plus bas) doit implémenter NotifyPropertyInterface, généralement via le trait NotifyProperty, et appeler propertyChanged() dans chaque setter :
<?php namespace sample\src\model; use CCMBenchmark\Ting\Entity\NotifyProperty; use CCMBenchmark\Ting\Entity\NotifyPropertyInterface; class City implements NotifyPropertyInterface { use NotifyProperty; protected $id = null; protected $name = null; public function setId($id) { $this->propertyChanged('id', $this->id, $id); $this->id = (int) $id; } public function getId() { return (int) $this->id; } public function setName($name) { $this->propertyChanged('name', $this->name, $name); $this->name = (string) $name; } public function getName() { return (string) $this->name; } }
Le mapping entre une entité et sa table vit dans le Repository, qui doit implémenter MetadataInitializer::initMetadata() et retourner un Metadata décrivant la table et ses champs :
<?php namespace sample\src\model; use CCMBenchmark\Ting\Repository\Metadata; use CCMBenchmark\Ting\Repository\MetadataInitializer; use CCMBenchmark\Ting\Serializer\SerializerFactoryInterface; class CityRepository extends \CCMBenchmark\Ting\Repository\Repository implements MetadataInitializer { public static function initMetadata(SerializerFactoryInterface $serializerFactory, array $options = []) { $metadata = new Metadata($serializerFactory); $metadata->setEntity('sample\src\model\City'); $metadata->setConnectionName('main'); // doit correspondre à une clé de la config des connexions $metadata->setDatabase('world'); $metadata->setTable('t_city_cit'); $metadata->addField([ 'primary' => true, 'autoincrement' => true, 'fieldName' => 'id', 'columnName' => 'cit_id', 'type' => 'int', ]); $metadata->addField([ 'fieldName' => 'name', 'columnName' => 'cit_name', 'type' => 'string', ]); return $metadata; } }
MetadataRepository::batchLoadMetadata() scanne les fichiers correspondants, les charge (require), et appelle initMetadata() sur chaque classe implémentant MetadataInitializer pour enregistrer son Metadata.
Sous Symfony,
ting_bundlepermet de déclarer ce mapping avec les attributs#[Schema\Table]/#[Schema\Column]directement sur l'entité, plutôt que d'écrireinitMetadata()à la main.
Récupérer un objet par ID
<?php
$cityRepository = $services->get('RepositoryFactory')->get('\sample\src\model\CityRepository');
## Récupérer une ville par son id :
$city = $cityRepository->get(3);
Requête simple
<?php
# Cette requête accepte la même syntaxe qu'une requête préparée, mais reste une requête classique
$query = $cityRepository->getQuery(
"select cit_id, cit_name, c.cou_code, cit_district, cit_population, last_modified,
co.cou_code, cou_name, cou_continent, cou_region, cou_head_of_state
from t_city_cit as c
inner join t_country_cou as co on (c.cou_code = co.cou_code)
where co.cou_code = :code limit 3"
);
$collection = $query->setParams(['code' => 'FRA'])->query();
foreach ($collection as $result) {
var_dump($result);
echo str_repeat("-", 40) . "\n";
}
Requête préparée
<?php
// Requête simple :
$query = $cityRepository->getQuery('SQL Statement');
// Requête préparée :
$query = $cityRepository->getPreparedQuery('SQL Statement');
QueryBuilder fourni par aura/sqlquery
<?php
$queryBuilder = $cityRepository->getQueryBuilder($cityRepository::QUERY_SELECT);
$queryBuilder
->cols(['cit_id', 'cit_name as name'])
->from('t_city_cit');
$query = $cityRepository->getQuery($queryBuilder->getStatement());
Écriture : l'UnitOfWork
Ting suit les changements sur les entités gérées et regroupe les écritures via UnitOfWork, récupéré via $services->get('UnitOfWork').
<?php $unitOfWork = $services->get('UnitOfWork'); $city = $cityRepository->get(3); $city->setName('Boum'); $unitOfWork->pushSave($city); // marque une mise à jour (entité déjà connue/gérée) $newCity = new \sample\src\model\City(); $newCity->setName('Bouh'); $unitOfWork->pushSave($newCity); // marque une insertion (entité nouvelle) $unitOfWork->process(); // exécute tous les insert/update en attente sur la base $unitOfWork->pushDelete($newCity); $unitOfWork->process(); // exécute la suppression
Autres méthodes utiles : isManaged(), isNew(), shouldBePersisted(), shouldBeRemoved(), detach()/detachAll() (arrêter de suivre une entité). Les transactions se gèrent au niveau du repository : $repository->startTransaction() / $repository->rollback().
Hydrators
Une Query hydrate son jeu de résultats via un hydrator ($query->query(new Collection($hydrator))). Plusieurs hydrators sont disponibles via $services, selon la forme de données souhaitée :
Hydrator(par défaut) : hydrate uneCollectiond'entités.HydratorSingleObject: hydrate une ligne en un seul objet agrégat, utile avecmapAliasTo()(mapper une colonne virtuelle/aliasée commeCOUNT(*)ouNOW()vers un setter) etmapObjectTo()(mapper les colonnes d'une table jointe vers un objet lié).HydratorAggregator: agrège plusieurs lignes en groupes (ex : un parent avec une liste d'enfants), piloté parcallableIdIs()/callableDataIs().HydratorRelational: version déclarative de ce qui précède, viaCCMBenchmark\Ting\Repository\Hydrator\RelationOne/RelationMany(avecAggregateFrom/AggregateTo) pour décrire des relations one-to-one et one-to-many entre les tables jointes.HydratorArray: hydrate les lignes sous forme de tableaux plutôt que d'entités.
<?php $query = $cityRepository->getQuery(" select t_city_cit.*, t_country_cou.* from t_city_cit left join t_country_cou on t_country_cou.cou_code = t_city_cit.cou_code "); $hydrator = $services->get('HydratorRelational'); $hydrator->addRelation(new \CCMBenchmark\Ting\Repository\Hydrator\RelationOne( new \CCMBenchmark\Ting\Repository\Hydrator\AggregateFrom('t_country_cou', 'getCode'), new \CCMBenchmark\Ting\Repository\Hydrator\AggregateTo('t_city_cit', 'getId'), 'countryIs' // setter appelé sur l'entité City )); $collection = $query->query(new \CCMBenchmark\Ting\Repository\Collection($hydrator)); foreach ($collection as $city) { echo $city->getName() . ' - ' . $city->getCountry()->getName() . "\n"; }
Voir sample/src/main.php et sample/src/mainUnitOfWork.php pour des exemples complets et exécutables (requête simple, requête préparée, UnitOfWork, hydratation agrégée et relationnelle).
Serializers
Les serializers convertissent une valeur PHP vers/depuis sa représentation en base, utilisés à la fois pour les champs d'entité (via le type de Metadata::addField()) et pour les colonnes virtuelles/aliasées (via HydratorSingleObject::unserializeAliasWith()). Disponibles dans CCMBenchmark\Ting\Serializer :
DateTime/DateTimeImmutable/DateTimeZoneBackedEnumUuid(nécessitesymfony/uid)IpJsonGeometry(nécessitebrick/geo, pour les colonnes Geometry MariaDB)
Drivers
Ting fournit des drivers pour :
- MySQL (
CCMBenchmark\Ting\Driver\Mysqli, nécessiteext-mysqli) - PostgreSQL (
CCMBenchmark\Ting\Driver\Pgsql, nécessiteext-pgsql) - SphinxQL (
CCMBenchmark\Ting\Driver\SphinxQL, parle le protocole MySQL)
Le driver utilisé par une connexion est sélectionné via la clé namespace de sa config (voir Bootstrap).