xavierleune / ting_bundle
Symfony bundle for xavierleune/ting. Provides an interface to use a lightweight datamapper in symfony.
Package info
github.com/xavierleune/ting_bundle
Type:symfony-bundle
pkg:composer/xavierleune/ting_bundle
Requires
- php: >=8.2
- symfony/cache: ^7.4 || ^8.0
- symfony/config: ^7.4 || ^8.0
- symfony/dependency-injection: ^7.4 || ^8.0
- symfony/http-kernel: ^7.4 || ^8.0
- symfony/property-access: ^7.4 || ^8.0
- symfony/service-contracts: ^2.5 || ^3.0
- symfony/stopwatch: ^7.4 || ^8.0
- symfony/validator: ^7.4 || ^8.0
- xavierleune/ting: ^4.0
Requires (Dev)
- brick/geo: 0.13.*
- phpunit/phpunit: ^11.5 || ^12.0
- symfony/expression-language: ^7.4 || ^8.0
- symfony/framework-bundle: ^7.4 || ^8.0
- symfony/security-bundle: ^7.4 || ^8.0
- symfony/security-core: ^7.4 || ^8.0
- symfony/serializer: ^7.4 || ^8.0
- symfony/uid: ^7.4 || ^8.0
Suggests
None
Provides
None
Conflicts
None
Replaces
- ccmbenchmark/ting_bundle: 4.0.0
- dev-main
- 4.0.0
- 3.12.0
- 3.11.0
- 3.10.1
- 3.10.0
- 3.9.0
- 3.8.2
- 3.8.1
- 3.8.0
- 3.7.0
- 3.6.1
- 3.6.0
- 3.5.0
- 3.4.2
- 3.4.1
- 3.4.0
- 3.3.2
- 3.3.1
- 3.3.0
- 3.2.3
- 3.2.2
- 3.2.1
- 3.2.0
- 3.1.0
- 3.0.1
- 3.0.0
- 2.2.0
- 2.1.0
- 2.0.5
- 2.0.4
- 2.0.3
- 2.0.2
- 2.0.1
- 2.0.0
- 1.2.0
- 1.1.2
- 1.1.1
- 1.1.0
- 1.0.0
- dev-release/4.0.0
- dev-docs/readme
- dev-chore/ting-4.0
- dev-docs/flex-package-switch
- dev-docs/cleanup
- dev-feature/table-defaults
- dev-fix/readme-and-property-types
- dev-feature/worker-mode
- dev-feature/poc-simple
- dev-fix/allow-null-sf-serializer
- dev-fix/attr-autoinc
- dev-feature/3.10.0
- dev-feature/unique-entity-attribute
- dev-feature/user-provider-2
- dev-feature/value-resolver
- dev-feature/user-provider
- dev-fix/uuid-serializer
- dev-fix/php84
- dev-feature/public-properties
- dev-master
- dev-feature/sf7
This package is auto-updated.
Last update: 2026-10-16 16:04:06 UTC
README
Ting for Symfony. Map your entities with PHP attributes, inject their repositories, get entities straight from your routes, and see every query in the profiler.
#[Route('/cities/{id:city}/archive', methods: ['POST'])] public function archive(City $city, CityRepository $cityRepository): Response { $city->status = CityStatus::Archived; $cityRepository->save($city); // UPDATE city SET status = 'archived' WHERE id = ... return new Response(status: 204); }
Why ting_bundle
- Entities mapped with attributes.
#[Table]and#[Column]on your own classes: the type of each column comes from the type of its property, enums, dates, UUIDs, JSON, geometries and value objects included. - Repositories as services. Autowire them anywhere. They are reset between requests, ready for worker mode (FrankenPHP, RoadRunner).
- Entities in your controllers. Route parameters are resolved to entities;
#[MapEntity]covers criteria and expressions. - Security and validation. A
tinguser provider, and aUniqueEntityconstraint for the validator. - Ting in the profiler. Every query and cache operation of the request, with its duration, in the web profiler.
- Symfony cache. Ting's query cache uses any Symfony cache pool, and the metadata is warmed up with the container.
ting_bundle needs PHP 8.2 or later, Symfony 7.4 or 8 (Symfony 8 needs PHP 8.4) and Ting 4.
Installation
composer require xavierleune/ting_bundle
Without Symfony Flex, register the bundle in config/bundles.php:
return [ // ... CCMBenchmark\TingBundle\TingBundle::class => ['all' => true], ];
New to Ting? The getting started guide builds a small application, from the configuration to the first queries.
Quick tour
Configure
# config/packages/ting.yaml ting: connections: main: namespace: CCMBenchmark\Ting\Driver\Mysqli charset: utf8mb4 primary: host: '%env(DATABASE_HOST)%' user: '%env(DATABASE_USER)%' password: '%env(DATABASE_PASSWORD)%' port: 3306 # Used by the entities that do not name their connection or database default_connection: main default_database: app
Map an entity
An entity is a class of your own. #[Table] links it to its table and repository; each mapped property gets a
#[Column], and its type comes from the type of the property. Ting writes the properties whose changes are notified
with propertyChanged(), here in a PHP 8.4 property hook (a setter works too).
<?php namespace App\Entity; use App\Repository\CityRepository; use CCMBenchmark\Ting\Entity\NotifyProperty; use CCMBenchmark\Ting\Entity\NotifyPropertyInterface; use CCMBenchmark\TingBundle\Schema\Column; use CCMBenchmark\TingBundle\Schema\Table; #[Table(name: 'city', repository: CityRepository::class)] class City implements NotifyPropertyInterface { use NotifyProperty; #[Column(autoIncrement: true, primary: true)] public ?int $id = null; #[Column] public string $name = '' { set(string $name) { $this->propertyChanged('name', $this->name, $name); $this->name = $name; } } #[Column] // a backed enum: the value of the case is stored public CityStatus $status = CityStatus::Active { set(CityStatus $status) { $this->propertyChanged('status', $this->status, $status); $this->status = $status; } } }
<?php namespace App\Entity; enum CityStatus: string { case Active = 'active'; case Archived = 'archived'; }
<?php namespace App\Repository; use App\Entity\City; use CCMBenchmark\Ting\Repository\Repository; /** @extends Repository<City> */ class CityRepository extends Repository { }
The bundle reads #[Table] through Symfony's autoconfiguration: src/Entity/ must not be excluded from the services
of config/services.yaml (see the guide).
Use it in your controllers
Inject the repository, and let the route give you the entity:
<?php namespace App\Controller; use App\Entity\City; use App\Entity\CityStatus; use App\Repository\CityRepository; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\Routing\Attribute\Route; class CityController { public function __construct(private readonly CityRepository $cityRepository) { } #[Route('/cities')] public function list(): JsonResponse { $cities = $this->cityRepository->getBy(['status' => CityStatus::Active], order: ['name' => 'ASC'], limit: 20); return new JsonResponse(array_map(fn (City $city) => $city->name, $cities->toArray())); } #[Route('/cities/{id:city}')] public function show(City $city): JsonResponse // 404 when no city has this id { return new JsonResponse(['id' => $city->id, 'name' => $city->name]); } }
Reading, writing, transactions and your own SQL are Ting's: see its documentation.
Validate and authenticate
#[Table(name: 'users', repository: UserRepository::class)] #[UniqueEntity(fields: ['email'], repository: UserRepository::class)] class User implements UserInterface, PasswordAuthenticatedUserInterface, NotifyPropertyInterface { // ... }
# config/packages/security.yaml security: providers: app_user_provider: ting: class: App\Entity\User property: email
Reference
Configuration
ting: connections: main: namespace: CCMBenchmark\Ting\Driver\Mysqli # or CCMBenchmark\Ting\Driver\Pgsql charset: utf8mb4 primary: host: '%env(DATABASE_HOST)%' user: '%env(DATABASE_USER)%' password: '%env(DATABASE_PASSWORD)%' port: 3306 # Optional: reads go to a replica, writes to the primary replicas: replica1: host: '%env(DATABASE_REPLICA_HOST)%' user: '%env(DATABASE_USER)%' password: '%env(DATABASE_PASSWORD)%' port: 3306 databases_options: app: timezone: 'Europe/Paris' # Optional: connection and database of the #[Table] attributes that do not give them default_connection: main default_database: app # Optional: id of a Symfony cache pool, used by cached queries (a NullAdapter by default) cache_provider: cache.ting # Optional: how properties typed with a backed enum are stored # backed_enum (default): the value of the case; symfony_serializer: its JSON encoding, as ting_bundle < 4.0 enum_serializer: backed_enum
port is required for the primary and every replica. The cache pool is any Symfony cache pool:
framework: cache: pools: cache.ting: adapter: cache.adapter.redis
Repositories declaring their metadata in PHP (Ting's MetadataInitializer) instead of attributes are loaded with
repositories:
ting: repositories: app: namespace: App\Repository directory: '%kernel.project_dir%/src/Repository' glob: '*Repository.php' # default options: # Options of every repository: connection, database and extra (passed to initMetadata()) default: connection: main database: app # Options of one repository App\Repository\CityRepository: extra: foo: bar
#[Table]
CCMBenchmark\TingBundle\Schema\Table, on the entity class:
| Argument | Description |
|---|---|
name |
The table |
repository |
The repository class (required) |
connection |
The connection, default_connection of the configuration if omitted |
database |
The database, default_database of the configuration if omitted |
Building the container fails with a LogicException naming the entity when the connection or the database is neither
given nor configured, or when the repository is missing.
#[Column]
CCMBenchmark\TingBundle\Schema\Column, on every property mapped to a column. Its arguments, all optional:
| Argument | Description |
|---|---|
column |
The column, the property name in snake case by default (countryCode → country_code) |
primary |
Part of the primary key |
autoIncrement |
Generated by the database, set on the entity after an insert |
serializer |
A serializer class, instead of the one deduced from the type |
serializerOptions |
Options of the serializer |
mutable |
See Change tracking |
The type of the column comes from the type of the property, which must be a single type (nullable or not):
| Property type | Ting type / serializer |
|---|---|
string, int, float, bool |
string, int, double, bool |
array |
json, decoded as an associative array |
\DateTimeImmutable / \DateTime |
datetime_immutable / datetime (Y-m-d H:i:s) |
\DateTimeZone |
datetimezone |
Symfony\Component\Uid\Uuid and subclasses |
uuid |
Brick\Geo\Geometry subclasses |
geometry |
| backed enum | string, with Ting's BackedEnum serializer: the value of the case (see enum_serializer) |
| any other class (pure enum, value object...) | symfony_serializer (JSON), when symfony/serializer is installed |
Change tracking: mutable
A mutable value can be modified in place without a setter ($city->updatedAt->modify('+1 day')): Ting compares it
on save instead of relying on propertyChanged(). Ting decides from the serializer which values are mutable; the
bundle refines it from the type of the property:
- the values of a custom serializer (including
symfony_serializer) are immutable when the property type is a scalar, an array, an enum, a\DateTimeImmutableor areadonlyclass; #[Column(mutable: true|false)]overrides both.
Prefer \DateTimeImmutable dates and readonly value objects. See
Tracking changes in Ting's
documentation.
Setters and uninitialized properties
Before PHP 8.4, use private properties, or public ones with a setter calling propertyChanged():
public string $name; public function setName(string $name): void { $this->propertyChanged('name', $this->name ?? null, $name); $this->name = $name; }
Ting bypasses property hooks when it hydrates an entity. An uninitialized typed property is ignored when the entity is saved: the column needs a default value in the database.
Value resolver and #[MapEntity]
An argument typed with an entity is resolved from the route:
{id:city}: theCitywhoseidproperty matches the parameter, injected as$city;{id}: a parameter namedidis the primary key;- otherwise, the route parameters named after properties of the entity are the criteria.
The resolver answers 404 when no entity matches, unless the argument is nullable. #[MapEntity]
(CCMBenchmark\TingBundle\Attribute\MapEntity) configures it:
| Argument | Description |
|---|---|
id |
The route parameter(s) holding the primary key |
mapping |
Route parameter → property, used as criteria |
exclude |
Route parameters not used as criteria |
stripNull |
Ignore the parameters that are null |
expr |
An expression returning the entity |
forcePrimary |
Read from the primary server instead of a replica |
class |
The entity class, when the argument type is not enough |
message |
The message of the 404 |
disabled |
Do not resolve this argument |
The expression receives repository (the repository of the entity), request, and every request attribute as a
variable: the route parameters are available by their name (by their mapped name for {parameter:name}), whether or
not the action has an argument for them.
#[Route('/users/{firstname}/{lastname}')] public function show(#[MapEntity(expr: 'repository.getOneBy({"firstname": firstname, "lastname": lastname})')] User $user): JsonResponse
User provider
The ting user provider loads users with their repository: by the property given in its configuration, or, without
it, by the loadUserByIdentifier() method of the repository, which then implements
CCMBenchmark\TingBundle\Security\UserLoaderInterface.
The user class implements Symfony's UserInterface (and PasswordAuthenticatedUserInterface for password
authenticated users). It also implements __serialize(): the user is serialized in the session, keep only what
identifies it (and the password hash), not other sensitive data. While supporting Symfony 7.4, keep an empty
eraseCredentials() marked #[\Deprecated], as Symfony recommends.
UniqueEntity
CCMBenchmark\TingBundle\Validator\Constraints\UniqueEntity, on the entity class, checks that no other row has the
same values:
| Argument | Description |
|---|---|
fields |
The property, or properties, that must be unique together |
repository |
The repository class of the entity |
identityFields |
The properties identifying the entity, so that it does not conflict with itself when updated |
message |
The violation message |
groups, payload |
As for any constraint |
Passing an array of options (options: [...]) was deprecated in 3.12 and is no longer supported since 4.0.
Profiler and worker mode
In debug mode, the web profiler gets a Ting panel for the queries (with their parameters and duration) and one for the cache operations. The connection pool, the unit of work and the repositories are reset between requests, so that worker mode (FrankenPHP, RoadRunner) does not leak state from one request to the next.
Documentation
- Getting started with Symfony
- Ting's documentation: entities, repositories, queries, hydrators, unit of work, cache
- Changelog
- Upgrading from 3.x
- Issues
Coming from ccmbenchmark/ting_bundle
ting_bundle was created at CCM Benchmark Group, where it is no longer
maintained; development continues here as xavierleune/ting_bundle. The PHP namespaces (CCMBenchmark\TingBundle\...)
are unchanged: change the package name in your composer.json and follow UPGRADE-4.0.md. With
Symfony Flex, check config/bundles.php afterwards: see
Switching from ccmbenchmark/ting_bundle.