Search by

letkode / orm-toolkit-bundle

letkode.app

Doctrine entity traits, value objects and DQL utilities for Symfony applications

Package info

github.com/letkode/orm-toolkit-bundle

Type:symfony-bundle

pkg:composer/letkode/orm-toolkit-bundle

Statistics

Installs: 395

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

3.0.0 2026-11-02 00:28 UTC

This package is auto-updated.

Last update: 2026-11-02 00:53:49 UTC


README

Doctrine entity traits, repository traits, value objects and DQL utilities for Symfony applications.

Installation

composer require letkode/orm-toolkit-bundle

Symfony Flex will register the bundle automatically. If not using Flex, add it manually:

// config/bundles.php
return [
    Letkode\OrmToolkitBundle\LetkodeOrmToolkitBundle::class => ['all' => true],
];

The bundle automatically registers the TRANSLATE_FIELD_VALUE Doctrine DQL function via PrependExtensionInterface.

Entity Traits

UuidTrait

Adds a uuid column (UUIDv7) with a PostgreSQL uuidv7() default. The UuidGeneratorSubscriber ensures PHP-side generation before persist so Gedmo Loggable captures the value.

use Letkode\OrmToolkitBundle\Trait\Entity\UuidTrait;

#[ORM\Entity]
class Product
{
    use UuidTrait;
}

HasTranslationsTrait moved to letkode/locale-bundle (Letkode\LocaleBundle\Trait\HasTranslationsTrait) — see that package's README. doctrine/orm is suggest-only there, so it stays an opt-in dependency for apps that don't need entity-level translations.

ParameterTrait

Adds a parameters jsonb column with recursive merge support.

$entity->setParameter('color', 'red');
$entity->getParameter('color'); // 'red'
$entity->setParameters(['size' => 'L'], force: false); // recursive merge

ObjectTrackNullableTrait / ObjectTrackRequiredTrait

Adds objectClass and objectId columns to track which object a record belongs to. Use the nullable variant when the relation is optional.

Repository Traits

BaseRepositoryTrait

Query/filter/pagination params are provided by letkode/list-query-bundle.

use Letkode\ListQueryBundle\Request\ListQueryRequest;
use Letkode\ListQueryBundle\Sort\SortInput;

$repo->save($entity);
$repo->remove($entity);
$repo->findByUuid($uuid);           // returns T|null
$repo->findOrFailByUuid($uuid);     // throws Letkode\HttpExceptionBundle\Exception\EntityNotFoundException (404)
$repo->paginate($qb, $query, sortable: ['name' => SortInput::field()], searchable: ['name', 'email']);
// $query is a Letkode\ListQueryBundle\Request\ListQueryRequest

searchable entries may be a qualified alias path ('p.lastName') instead of a bare field name, to search on a joined entity:

$qb = $this->createQueryBuilder('c')->join('c.person', 'p');

$repo->paginate($qb, $query, searchable: ['p.firstName', 'p.lastName', 'p.email']);

sortable is declared like filterable: the array key is the public name the client sends, and the value is a SortInput that names the alias that owns the field, so the query alias never reaches the API. Every entry must be a SortInput (and every filterable entry a FilterInput); anything else throws an InvalidArgumentException.

$repo->paginate($qb, $query, sortable: [
    'createdAt' => SortInput::field(),                              // root alias
    'legalName' => SortInput::field(alias: 'c'),                    // ?sort=legalName -> c.legalName
    'company_name' => SortInput::field(alias: 'c', property: 'name'), // explicit property
]);

The property defaults to the key converted to the configured property_case. A SortInput is matched by its key only: the qualified path (c.legalName) is not accepted.

A bare searchable field name (no dot) resolves against the root alias. The referenced alias must already be joined on the QueryBuilder passed to paginate() — the bundle does not validate or infer joins, same responsibility as FilterInput::alias. Ordering on a path behind a to-many join can duplicate rows in the paginated result (the usual Doctrine to-many fan-out); prefer sorting/searching on a to-one relation, or on a column of the root entity, when possible.

Strict mode

paginate() validates the request against the declared allowlists before touching the database. By default (strict: true) an undeclared sort field, an undeclared filter field, an unknown filter operator or a malformed filter entry throws Letkode\ListQueryBundle\Exception\UndeclaredQueryParameterException, which carries the full list of rejections (->rejections) so the caller can report them all at once and it is an HTTP status exception (422, with the errors by parameter), so the ExceptionListener of letkode/http-exception-bundle renders it without any extra code. A q shorter than minSearchLength is not a rejection — search is simply not applied.

$repo->paginate($qb, $query, sortable: ['name' => SortInput::field()], strict: false); // silently ignore rejections

TranslatableRepositoryTrait

$this->addTranslatedOrderBy($qb, 'p', 'name', $locale, 'ASC');
$this->addTranslatedSearch($qb, 'p', 'name', $searchTerm, $locale);

Value Objects

All value objects are final readonly, normalize on construction and throw ValueObjectException on invalid input.

Class Validates
Email Valid email, lowercased
Phone E.164-compatible (strips spaces/dashes)
Slug Lowercase, [a-z0-9-], 2–255 chars
Username [a-zA-Z0-9_.-], 3–50 chars
$email = new Email('  USER@Example.COM  '); // 'user@example.com'
$slug  = new Slug('My Product Name');        // 'my-product-name'

DQL Function

TRANSLATE_FIELD_VALUE(column, 'field', :locale) maps to jsonb_extract_path_text(column, locale, field).

Useful for ordering and filtering on translated values stored in a jsonb translations column.

Requirements

  • PHP ^8.4
  • Symfony ^7.0 || ^8.0
  • doctrine/orm ^3.0
  • doctrine/doctrine-bundle ^3.0
  • gedmo/doctrine-extensions ^3.0
  • letkode/http-exception-bundle ^1.2
  • letkode/list-query-bundle ^1.0

License

MIT — see LICENSE.

Property case

A filter's target is built as alias.property: the input's alias (the root alias when omitted) and its property (the key converted to your entity properties' spelling when omitted). The property_case option and the per-input propertyCase argument belong to letkode/list-query-bundle (letkode_list_query.property_case); see its README. 'legal_name' => FilterInput::text() then filters on <root>.legalName, FilterInput::text(alias: 'c') on c.legalName, and FilterInput::text(expression: "CONCAT(u.firstName, ' ', u.lastName)") filters on that DQL expression as-is.