PHPackages                             danilovl/entity-traits-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. danilovl/entity-traits-bundle

ActiveSymfony-bundle[Database &amp; ORM](/categories/database)

danilovl/entity-traits-bundle
=============================

Symfony bundle providing reusable Doctrine ORM entity traits, interfaces, listeners and validators to eliminate Entity boilerplate.

v0.0.1(2mo ago)00MITPHPPHP ^8.5CI passing

Since Apr 26Pushed 2mo agoCompare

[ Source](https://github.com/danilovl/entity-traits-bundle)[ Packagist](https://packagist.org/packages/danilovl/entity-traits-bundle)[ RSS](/packages/danilovl-entity-traits-bundle/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (22)Versions (2)Used By (0)

[![phpunit](https://github.com/danilovl/entity-traits-bundle/actions/workflows/phpunit.yml/badge.svg)](https://github.com/danilovl/entity-traits-bundle/actions/workflows/phpunit.yml)[![downloads](https://camo.githubusercontent.com/1a4465e792970c501d2e08bf3560abd28e1bd315345c1e5ecefd9c9258d41931/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f64616e696c6f766c2f656e746974792d7472616974732d62756e646c65)](https://packagist.org/packages/danilovl/entity-traits-bundle)[![latest Stable Version](https://camo.githubusercontent.com/10058d4a98ab68667eb1f9153d1c7201ccc60e385933ae4a2043f77c042f46db/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f64616e696c6f766c2f656e746974792d7472616974732d62756e646c65)](https://packagist.org/packages/danilovl/entity-traits-bundle)[![license](https://camo.githubusercontent.com/d41d8d594144bf9e190bf712e428e045d92772c02e66e26effb4e3eca8f78852/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f64616e696c6f766c2f656e746974792d7472616974732d62756e646c65)](https://packagist.org/packages/danilovl/entity-traits-bundle)

EntityTraitsBundle
==================

[](#entitytraitsbundle)

Symfony bundle providing **410 Doctrine ORM trait variants** across **21 categories** — plus interfaces, listeners, filters, validators and attributes — to eliminate the boilerplate that gets copy-pasted across every entity (`id`, `createdAt`, `updatedAt`, `slug`, `active`, `deletedAt`, …).

Requirements
------------

[](#requirements)

- PHP **8.5+**
- Symfony **8.0**
- Doctrine ORM **3.0**

Installation
------------

[](#installation)

```
composer require danilovl/entity-traits-bundle
```

If you are not using Symfony Flex, register the bundle in `config/bundles.php`:

```
return [
    // ...
    Danilovl\EntityTraitsBundle\EntityTraitsBundle::class => ['all' => true],
];
```

Then create `config/packages/danilovl_entity_traits.yaml`:

```
danilovl_entity_traits:
    identity:
        default_strategy: int       # int | uuid | ulid — used by #[AutoIdentifier]
    timestampable:
        enabled: true
        timezone: 'UTC'             # timezone applied to all auto-timestamps
    blameable:
        enabled: false
        user_class: App\Entity\User # required when enabled
        use_username_string: false  # store getUserIdentifier() string instead of User object
    sluggable:
        enabled: true
        fallback_field: 'name'
    soft_delete:
        enabled: true
        filter_auto_enable: true    # auto-registers SoftDeleteFilter in Doctrine
    request_context:
        user_agent_max_length: 500
    last_login_at:
        throttle_seconds: 0         # skip update if last login is within N seconds
    tags:
        lowercase: true
```

Required vs Optional variants
-----------------------------

[](#required-vs-optional-variants)

Every trait and `*AwareInterface` ships in **two variants**, mirroring whether the underlying field is mandatory or optional at the database level:

VariantFolderDoctrinePHP type`Required``Trait/Required//``nullable: false`non-nullable (`string`, `int`, …)`Optional``Trait/Optional//``nullable: true`nullable (`?string`, `?int`, …)Pick whichever matches your column constraint. You can mix freely within an entity:

```
use Danilovl\EntityTraitsBundle\Trait\Required\Content\NameTrait;     // string $name
use Danilovl\EntityTraitsBundle\Trait\Optional\Content\TitleTrait;    // ?string $title = null
```

The same split applies to interfaces under `Contract/Required//` and `Contract/Optional//`. Listeners and filters in this bundle accept **both** variants — they `instanceof`-check both contract namespaces internally, so user entities are free to pick either side.

Quick start
-----------

[](#quick-start)

```
namespace App\Entity;

use Danilovl\EntityTraitsBundle\Contract\Optional\Audit\BlameableInterface;
use Danilovl\EntityTraitsBundle\Contract\Optional\Timestampable\TimestampableInterface;
use Danilovl\EntityTraitsBundle\Trait\Optional\Audit\BlameableTrait;
use Danilovl\EntityTraitsBundle\Trait\Optional\Content\{ContentTrait, TitleTrait};
use Danilovl\EntityTraitsBundle\Trait\Optional\Counter\ViewsCountTrait;
use Danilovl\EntityTraitsBundle\Trait\Required\Identity\UuidTrait;
use Danilovl\EntityTraitsBundle\Trait\Optional\Seo\{MetaTrait, SlugTrait};
use Danilovl\EntityTraitsBundle\Trait\Optional\Timestampable\{PublishedAtTrait, TimestampableTrait};
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'articles')]
class Article implements TimestampableInterface, BlameableInterface
{
    use UuidTrait;
    use TitleTrait;
    use SlugTrait;
    use ContentTrait;
    use MetaTrait;
    use PublishedAtTrait;
    use TimestampableTrait;
    use BlameableTrait;
    use ViewsCountTrait;
}
```

That's an entity with 11 columns and 30+ helper methods (`getCreatedAt()`, `setSlug()`, `publish()`, `isPublished()`, `incrementViewsCount()`, …) without writing a single field by hand.

The bundle's listeners do the rest:

- `TimestampableListener` populates `createdAt` / `updatedAt` in the configured timezone.
- `SluggableListener` generates the slug from `title` / `name` when empty.
- `BlameableListener` sets `createdBy` / `updatedBy` from the current Security user (object or string identifier).
- `SoftDeletableListener` converts `$em->remove($entity)` into `UPDATE … SET deleted_at = NOW()`.
- `RequestContextListener` populates `ipAddress` / `userAgent` from the current Request.
- `LastLoginAtListener` updates `lastLoginAt` on `LoginSuccessEvent`.
- `RevisionListener` increments `revision` on every `onFlush` update.
- `TreePathListener` rebuilds materialized `path` / `depth` for entities marked with `#[Tree]`.
- `TagsNormalizationListener` trims, lowercases and dedupes `tags` arrays.
- `AutoIdentifierListener` fills properties marked with `#[AutoToken]`, `#[AutoUuid]`, `#[AutoUlid]`, `#[AutoIdentifier]`.

Trait catalog
-------------

[](#trait-catalog)

205 Optional + 205 Required = **410 trait variants** across **21 categories**. Every trait has a paired `*AwareInterface` under `Contract/Required//` and `Contract/Optional//`.

GroupTraits**Identity**`IdTrait`, `UuidTrait`, `UlidTrait`**Timestampable**`CreatedAtTrait`, `UpdatedAtTrait`, `DeletedAtTrait`, `PublishedAtTrait`, `StartAtTrait`, `EndAtTrait`, `ExpiresAtTrait`, `ConfirmedAtTrait`, `ApprovedAtTrait`, `LastLoginAtTrait`, `ArchivedAtTrait`, `LockedAtTrait`, `ScheduledAtTrait`, `ProcessedAtTrait`, `CompletedAtTrait`, `FailedAtTrait`, `SentAtTrait`, `ReadAtTrait`, `AcceptedAtTrait`, `RejectedAtTrait`, `SuspendedAtTrait`, `RetiredAtTrait`, `TrialEndsAtTrait`, `SubscribedAtTrait`, `UnsubscribedAtTrait`, `NextBillingAtTrait`, `VerifiedAtTrait`, `BannedAtTrait`, `TimestampableTrait`, `FullTimestampableTrait`, `SoftDeletableTrait`**Status / Flags**`ActiveTrait`, `EnabledTrait`, `VisibleTrait`, `FeaturedTrait`, `PinnedTrait`, `VerifiedTrait`, `PublicTrait`, `WorkflowTrait`, `DraftTrait`, `SuspendedTrait`, `BannedTrait`, `ReadTrait`, `ProcessedTrait`, `ApprovedTrait`, `SpamTrait`, `LockedTrait`**Content**`NameTrait`, `TitleTrait`, `SubtitleTrait`, `DescriptionTrait`, `ContentTrait`, `ExcerptTrait`, `NotesTrait`, `CodeTrait`, `ReferenceTrait`**People**`FirstNameTrait`, `LastNameTrait`, `MiddleNameTrait`, `FullNameTrait`, `AcademicDegreeTrait`, `BirthdayTrait`, `GenderTrait`, `NationalityTrait`, `AvatarTrait`, `UsernameTrait`, `DisplayNameTrait`, `NicknameTrait`, `HeadlineTrait`, `PronounsTrait`, `BiographyTrait`**Contact**`EmailTrait`, `EmailConfirmableTrait`, `PhoneTrait`, `MobilePhoneTrait`, `WebsiteTrait`, `SocialLinksTrait`, `AddressTrait`, `SecondaryEmailTrait`, `StreetAddressTrait`, `EmergencyContactTrait`**Geographic**`LocationTrait`, `TimezoneTrait`, `LocaleTrait`, `CountryTrait`, `CurrencyTrait`**Geolocation**`RegionTrait`, `CityTrait`, `PostalCodeTrait`, `AddressLineTrait`**Media**`ImageTrait`, `ThumbnailTrait`, `FileTrait`, `VideoTrait`, `AudioTrait`, `AltTextTrait`, `CaptionTrait`, `DurationTrait`, `AttachmentsTrait`, `GalleryTrait`**SEO**`SlugTrait`, `MetaTrait`, `OgMetaTrait`, `CanonicalUrlTrait`, `RobotsTrait`, `TwitterCardTrait`, `SitemapTrait`, `HreflangTrait`, `SchemaOrgTrait`, `SlugAliasTrait`**Business**`PriceTrait`, `SkuTrait`, `BarcodeTrait`, `GtinTrait`, `IsbnTrait`, `StockTrait`, `WeightTrait`, `DimensionsTrait`, `TaxRateTrait`, `VatNumberTrait`, `DiscountTrait`, `CompareAtPriceTrait`, `CostPriceTrait`, `MinimumQuantityTrait`, `MaximumQuantityTrait`, `ReorderLevelTrait`, `UnitTrait`, `ManufacturerTrait`, `BrandTrait`, `ModelTrait`, `MpnTrait`**Pricing**`PriceRangeTrait`, `SubscriptionPriceTrait`, `TierPricingTrait`**Sorting / Hierarchy**`PriorityTrait`, `PositionTrait`, `LevelTrait`, `TreePathTrait`, `ParentTrait`**Audit / Blameable**`CreatedByTrait`, `UpdatedByTrait`, `DeletedByTrait`, `BlameableTrait`, `CreatedByStringTrait`, `UpdatedByStringTrait`, `OwnerTrait`, `AuthorTrait`, `IpAddressTrait`, `UserAgentTrait`**Versioning**`VersionTrait`, `RevisionTrait`**Counters**`ViewsCountTrait`, `LikesCountTrait`, `SharesCountTrait`, `CommentsCountTrait`, `DownloadsCountTrait`, `RatingTrait`, `FavoritesCountTrait`, `BookmarksCountTrait`, `FollowersCountTrait`, `FollowingCountTrait`, `SubscribersCountTrait`, `ClicksCountTrait`, `DislikesCountTrait`, `ErrorsCountTrait`, `PointsTrait`, `BalanceTrait`**Misc**`ColorTrait`, `IconTrait`, `TagsTrait`, `MetadataTrait`, `TokenTrait`, `HashTrait`, `SortKeyTrait`, `ChecksumTrait`, `LabelTrait`, `BadgeTrait`, `ExtrasTrait`, `SettingsTrait`, `FingerprintTrait`, `SecretTrait`, `EncryptedPayloadTrait`, `OriginalUrlTrait`**Security**`FailedLoginAttemptsTrait`, `LockedUntilTrait`, `TwoFactorEnabledTrait`, `ApiKeyTrait`, `PasswordChangedAtTrait`, `RememberTokenTrait`**Tenancy**`TenantTrait`, `OrganizationTrait`, `TeamTrait`, `WorkspaceTrait`**Integration**`ExternalIdTrait`, `ExternalSourceTrait`, `ExternalUrlTrait`, `ImportedAtTrait`, `SyncedAtTrait`**Presets**`BlogPostTrait`, `EcommerceProductTrait`, `UserAccountTrait`, `EventTrait`Conventions
-----------

[](#conventions)

TopicRuleDate typeAlways `\DateTimeImmutable`. Doctrine column type is `datetime_immutable` / `date_immutable`.Setter return`static` (fluent), never `void`.Property visibility`protected`, so subclasses (e.g. STI / `MappedSuperclass` heirs) can adjust mapping.Lifecycle logicLives in `EventListener/`, **not** inside traits.Boolean methods`isX()` / `hasX()`, never `getX()`.Identity strategyPick one of `IdTrait` / `UuidTrait` / `UlidTrait` per entity.Required `IdTrait`Property is uninitialized typed (`int $id;`); use `isNew()` (which uses `isset`) before reading `getId()`.Attributes
----------

[](#attributes)

AttributeEffect`#[AutoSlug(from: 'title')]`Class-level. Tells `SluggableListener` which field to slug from.`#[AutoToken(property: 'apiKey', length: 32)]`Generates a hex token (`bin2hex(random_bytes())`) on `prePersist` if empty.`#[AutoUuid]` / `#[AutoUlid]`Generates a UUID v7 / ULID for non-PK columns on `prePersist`.`#[AutoIdentifier(property: 'externalId')]`Generates a UUID v7 or ULID based on `identity.default_strategy`. Skipped for `int`.`#[Tree]`Marks a `ParentTrait` + `TreePathTrait` entity so `TreePathListener` rebuilds `path` / `depth` automatically.Validators
----------

[](#validators)

```
use Danilovl\EntityTraitsBundle\Trait\Optional\Geographic\LocationTrait;
use Danilovl\EntityTraitsBundle\Validator\{
    Barcode,
    Country,
    Currency,
    HexColor,
    Iban,
    Isbn,
    LatitudeRange,
    Locale,
    LongitudeRange,
    Timezone,
    VatNumber
};

class Product
{
    use LocationTrait;

    #[LatitudeRange]
    protected ?float $latitude = null;

    #[LongitudeRange]
    protected ?float $longitude = null;

    #[HexColor]
    protected ?string $brandColor = null;

    #[Iban]
    protected ?string $iban = null;

    #[VatNumber]
    protected ?string $vatNumber = null;

    #[Isbn]
    protected ?string $isbn = null;

    #[Barcode]   // EAN-8/EAN-13/UPC-A/GTIN-14 with checksum
    protected ?string $barcode = null;

    #[Currency]  // ISO 4217
    protected ?string $currency = null;

    #[Country]   // ISO 3166-1 alpha-2
    protected ?string $country = null;

    #[Locale]    // BCP 47
    protected ?string $locale = null;

    #[Timezone]  // IANA TZDB
    protected ?string $timezone = null;
}
```

All validators are registered as services with the `validator.constraint_validator` tag — Symfony's validator pipeline picks them up automatically.

Doctrine SQL filters
--------------------

[](#doctrine-sql-filters)

`soft_delete` is registered automatically when `soft_delete.filter_auto_enable: true` (the default). The remaining filters must be registered manually.

FilterAuto-registeredHides rows where…`SoftDeleteFilter`✅ by default`deleted_at IS NOT NULL``PublishedFilter`❌ manual`published_at IS NULL OR published_at > NOW()``ActiveFilter`❌ manual`active = false``ArchivedFilter`❌ manual`archived_at IS NOT NULL`To disable auto-registration of `SoftDeleteFilter`:

```
danilovl_entity_traits:
    soft_delete:
        filter_auto_enable: false
```

To register the other filters manually:

```
doctrine:
    orm:
        filters:
            published:
                class: Danilovl\EntityTraitsBundle\Doctrine\Filter\PublishedFilter
                enabled: false
```

```
$em->getFilters()->enable('published');
```

Enums
-----

[](#enums)

- `Gender` — `Male`, `Female`, `NonBinary`, `Other`, `PreferNotToSay`
- `IdentityStrategy` — `Int`, `Uuid`, `Ulid`
- `WorkflowState` — `Draft`, `Pending`, `InReview`, `Approved`, `Published`, `Rejected`, `Archived`, `Cancelled`
- `RobotsDirective` — `Index`, `NoIndex`, `Follow`, `NoFollow`, `IndexFollow`, `NoIndexNoFollow`, `NoArchive`, `NoSnippet`

Configuration as a service
--------------------------

[](#configuration-as-a-service)

Bundle config flows into a typed, autowireable `EntityTraitsConfig` value object:

```
use Danilovl\EntityTraitsBundle\DependencyInjection\Config\EntityTraitsConfig;

final readonly class Audit
{
    public function __construct(private EntityTraitsConfig $config) {}
}
```

FieldTypeConfig key`blameableUserClass``string``blameable.user_class``blameableUseUsernameString``bool``blameable.use_username_string``sluggableFallbackField``string``sluggable.fallback_field``userAgentMaxLength``int``request_context.user_agent_max_length``lastLoginAtThrottleSeconds``int``last_login_at.throttle_seconds``tagsNormalizationLowercase``bool``tags.lowercase``identityDefaultStrategy``string``identity.default_strategy``timestampableTimezone``string``timestampable.timezone`Timestampable timezone
----------------------

[](#timestampable-timezone)

All auto-timestamps are converted to the configured timezone before being stored:

```
danilovl_entity_traits:
    timestampable:
        timezone: 'Europe/Berlin'
```

`TimestampableListener` calls `$clock->now()->setTimezone(new DateTimeZone($config->timestampableTimezone))` — so the stored `DateTimeImmutable` carries the correct offset even when the app server is in UTC.

Slug source override
--------------------

[](#slug-source-override)

By default `SluggableListener` uses the `name` field. To slug from `title` (or any other field):

```
use Danilovl\EntityTraitsBundle\Attribute\AutoSlug;

#[ORM\Entity]
#[AutoSlug(from: 'title')]
class Article { /* ... */ }
```

Auto-identifier with configured strategy
----------------------------------------

[](#auto-identifier-with-configured-strategy)

`#[AutoIdentifier]` generates a UUID or ULID depending on `identity.default_strategy`, making the choice central rather than per-entity:

```
danilovl_entity_traits:
    identity:
        default_strategy: uuid   # uuid | ulid  (int = no generation)
```

```
use Danilovl\EntityTraitsBundle\Attribute\AutoIdentifier;

#[AutoIdentifier(property: 'externalId')]
class Order
{
    private ?object $externalId = null;
}
```

On `prePersist`, `AutoIdentifierListener` generates `Uuid::v7()` (or `new Ulid`) and fills the property if it is empty. For `int`, no generation is performed — Doctrine handles auto-increment at the database level.

Soft delete
-----------

[](#soft-delete)

```
use Danilovl\EntityTraitsBundle\Contract\Optional\Timestampable\SoftDeletableInterface;
use Danilovl\EntityTraitsBundle\Trait\Required\Identity\UuidTrait;
use Danilovl\EntityTraitsBundle\Trait\Optional\Timestampable\SoftDeletableTrait;

class User implements SoftDeletableInterface
{
    use UuidTrait;
    use SoftDeletableTrait;
    // ...
}

// Anywhere in your app:
$em->remove($user);   // -> issues UPDATE users SET deleted_at = NOW()
$em->flush();
```

`SoftDeleteFilter` is auto-registered and enabled by default (see `soft_delete.filter_auto_enable`). All subsequent SELECTs hide the row automatically. To include deleted rows:

```
$em->getFilters()->disable('soft_delete');
```

If the entity also implements `DeletedByAwareInterface`, the listener fills `deletedBy` from the current `Security` user during the same flush.

Blameable — object vs string
----------------------------

[](#blameable--object-vs-string)

By default `BlameableListener` stores the full `UserInterface` object as a Doctrine relation. Use `use_username_string: true` to store the username string instead, which avoids a foreign key and is useful for audit logs, microservices, or any context where the user relation is not available:

```
danilovl_entity_traits:
    blameable:
        enabled: true
        use_username_string: true
```

```
use Danilovl\EntityTraitsBundle\Contract\Optional\Audit\{
    CreatedByStringAwareInterface,
    UpdatedByStringAwareInterface
};
use Danilovl\EntityTraitsBundle\Trait\Optional\Audit\{
    CreatedByStringTrait,
    UpdatedByStringTrait
};

class Article implements CreatedByStringAwareInterface, UpdatedByStringAwareInterface
{
    use CreatedByStringTrait;   // string $createdBy column
    use UpdatedByStringTrait;   // string $updatedBy column
}
```

`BlameableListener` detects the interface variant at runtime: when `use_username_string = true` it calls `$entity->setCreatedBy($user->getUserIdentifier())` instead of `$entity->setCreatedBy($user)`.

The standard object-based traits (`CreatedByTrait`, `UpdatedByTrait`) remain unchanged and work as before when the flag is `false`.

Tree (materialized path)
------------------------

[](#tree-materialized-path)

```
use Danilovl\EntityTraitsBundle\Attribute\Tree;
use Danilovl\EntityTraitsBundle\Trait\Optional\Sorting\{ParentTrait, TreePathTrait};
use Danilovl\EntityTraitsBundle\Trait\Required\Identity\IdTrait;

#[ORM\Entity]
#[Tree(separator: '/', pathProperty: 'path', depthProperty: 'depth', identifier: 'id')]
class Category
{
    use IdTrait;
    use ParentTrait;
    use TreePathTrait;
}
```

`TreePathListener` rebuilds `path` (e.g. `/1/4/9`) and `depth` on every save by walking the `parent` chain.

Testing
-------

[](#testing)

```
composer install
composer tests          # phpunit (Unit + Functional)
composer phpstan        # static analysis
composer cs-fixer-check # php-cs-fixer dry run
```

Functional tests use a minimal `TestKernel` (under `tests/Functional/Kernel/TestKernel.php`) that boots only `FrameworkBundle` + `EntityTraitsBundle` so wiring scenarios can be verified in isolation.

Security traits
---------------

[](#security-traits)

Prevent brute-force and track authentication state:

```
use Danilovl\EntityTraitsBundle\Trait\Optional\Security\{
    FailedLoginAttemptsTrait,
    LockedUntilTrait,
    TwoFactorEnabledTrait,
    PasswordChangedAtTrait
};

class User
{
    use FailedLoginAttemptsTrait;  // $failedLoginAttempts, $lastFailedLoginAt
    use LockedUntilTrait;          // $lockedUntil + isCurrentlyLocked()
    use TwoFactorEnabledTrait;     // $twoFactorEnabled, $twoFactorSecret
    use PasswordChangedAtTrait;    // $passwordChangedAt
}

$user->incrementFailedLoginAttempts();  // bumps counter + sets lastFailedLoginAt
$user->resetFailedLoginAttempts();      // zeroes counter
$user->isCurrentlyLocked();            // true when lockedUntil > now
```

Tenancy traits
--------------

[](#tenancy-traits)

Add multi-tenancy scoping with a single trait:

```
use Danilovl\EntityTraitsBundle\Trait\Required\Tenancy\TenantTrait;

class Invoice
{
    use TenantTrait;       // int $tenantId
    use OrganizationTrait; // int $organizationId
}
```

Integration traits
------------------

[](#integration-traits)

Track external system references without coupling your schema to foreign systems:

```
use Danilovl\EntityTraitsBundle\Trait\Optional\Integration\{
    ExternalIdTrait,
    ExternalSourceTrait,
    ExternalUrlTrait,
    ImportedAtTrait,
    SyncedAtTrait
};

class Product
{
    use ExternalIdTrait;     // $externalId — ID in the foreign system
    use ExternalSourceTrait; // $externalSource — e.g. 'shopify', 'magento'
    use ImportedAtTrait;     // $importedAt
    use SyncedAtTrait;       // $syncedAt
}
```

Pricing traits
--------------

[](#pricing-traits)

Separate pricing concerns from the base product:

```
use Danilovl\EntityTraitsBundle\Trait\Optional\Pricing\{
    PriceRangeTrait,
    SubscriptionPriceTrait,
    TierPricingTrait
};

class Plan
{
    use SubscriptionPriceTrait; // $monthlyPrice, $annualPrice
    use PriceRangeTrait;        // $priceMin, $priceMax
}
```

Extended counters
-----------------

[](#extended-counters)

```
use Danilovl\EntityTraitsBundle\Trait\Optional\Counter\{
    FavoritesCountTrait,
    FollowersCountTrait,
    PointsTrait,
    BalanceTrait
};

class UserProfile
{
    use FavoritesCountTrait;  // incrementFavoritesCount() / decrementFavoritesCount()
    use FollowersCountTrait;
    use PointsTrait;          // loyalty / gamification points
    use BalanceTrait;         // wallet balance (in cents)
}
```

Geolocation traits
------------------

[](#geolocation-traits)

Fine-grained address decomposition, separate from the combined `AddressTrait`:

```
use Danilovl\EntityTraitsBundle\Trait\Optional\Geolocation\{
    RegionTrait,
    CityTrait,
    PostalCodeTrait,
    AddressLineTrait
};
```

Extended SEO
------------

[](#extended-seo)

```
use Danilovl\EntityTraitsBundle\Trait\Optional\Seo\{
    TwitterCardTrait,   // twitterCard, twitterTitle, twitterDescription, twitterImage
    SitemapTrait,       // sitemapPriority (float), sitemapChangefreq
    HreflangTrait,      // hreflang JSON map — setHreflangLocale('de', '/de/page')
    SchemaOrgTrait,     // schemaOrg JSON (JSON-LD structured data)
    SlugAliasTrait      // slugAlias for redirect from old URLs
};
```

License
-------

[](#license)

The EntityTraitsBundle is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

###  Health Score

34

—

LowBetter than 75% of packages

Maintenance82

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity41

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

89d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/1f695c3614981d994e19a87e7b2c892768642a577d7eeeb1c6b4bf899b566557?d=identicon)[danilovl](/maintainers/danilovl)

---

Top Contributors

[![danilovl](https://avatars.githubusercontent.com/u/16228565?v=4)](https://github.com/danilovl "danilovl (1 commits)")

---

Tags

symfonyormdoctrineentitysluggabletimestampableBlameableSymfony Bundletraitssoft-delete

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/danilovl-entity-traits-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/danilovl-entity-traits-bundle/health.svg)](https://phpackages.com/packages/danilovl-entity-traits-bundle)
```

###  Alternatives

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k17.9M400](/packages/easycorp-easyadmin-bundle)[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.5k5.9M754](/packages/sylius-sylius)[web-auth/webauthn-framework

FIDO2/Webauthn library for PHP and Symfony Bundle.

515100.5k3](/packages/web-auth-webauthn-framework)[open-dxp/opendxp

Content &amp; Product Management Framework (CMS/PIM)

9421.6k64](/packages/open-dxp-opendxp)[flow-php/flow

PHP ETL - Extract Transform Load - Data processing framework

85036.3k](/packages/flow-php-flow)[chameleon-system/chameleon-base

The Chameleon System core.

1028.7k5](/packages/chameleon-system-chameleon-base)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
