定制 danilovl/entity-traits-bundle 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

danilovl/entity-traits-bundle

Composer 安装命令:

composer require danilovl/entity-traits-bundle

包简介

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

README 文档

README

phpunit downloads latest Stable Version license

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

  • PHP 8.5+
  • Symfony 8.0
  • Doctrine ORM 3.0

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

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

Variant Folder Doctrine PHP type
Required Trait/Required/<Group>/ nullable: false non-nullable (string, int, …)
Optional Trait/Optional/<Group>/ 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/<Group>/ and Contract/Optional/<Group>/. 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

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

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

Group Traits
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

Topic Rule
Date type Always \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 logic Lives in EventListener/, not inside traits.
Boolean methods isX() / hasX(), never getX().
Identity strategy Pick one of IdTrait / UuidTrait / UlidTrait per entity.
Required IdTrait Property is uninitialized typed (int $id;); use isNew() (which uses isset) before reading getId().

Attributes

Attribute Effect
#[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

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

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

Filter Auto-registered Hides 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

  • GenderMale, Female, NonBinary, Other, PreferNotToSay
  • IdentityStrategyInt, Uuid, Ulid
  • WorkflowStateDraft, Pending, InReview, Approved, Published, Rejected, Archived, Cancelled
  • RobotsDirectiveIndex, NoIndex, Follow, NoFollow, IndexFollow, NoIndexNoFollow, NoArchive, NoSnippet

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) {}
}
Field Type Config 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

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

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

#[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

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

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)

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

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

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

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

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

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

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

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

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

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

The EntityTraitsBundle is open-sourced software licensed under the MIT license.

danilovl/entity-traits-bundle 适用场景与选型建议

danilovl/entity-traits-bundle 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 0 次下载、GitHub Stars 达 0, 最近一次更新时间为 2026 年 04 月 26 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「orm」 「symfony」 「sluggable」 「timestampable」 「doctrine」 「Blameable」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 danilovl/entity-traits-bundle 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 danilovl/entity-traits-bundle 我们能提供哪些服务?
定制开发 / 二次开发

基于 danilovl/entity-traits-bundle 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 45
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-04-26