cycle/entity-behavior
Adds behavior attributes to Cycle ORM entities (UUID, timestamps, soft delete, optimistic lock, hooks, event listeners) plus an API to build custom behaviors. Use EventDrivenCommandGenerator when creating the ORM to enable event-driven commands.
## Getting Started
### **Minimal Setup**
1. **Installation**
```bash
composer require cycle/orm entity-behavior
Ensure your composer.json includes cycle/orm as a dependency (this package extends Cycle ORM's behavior).
Basic Configuration
Add the behavior to your entity definition in config/cycle.php:
'behaviors' => [
Cycle\Annotated\EntityBehavior::class,
Cycle\EntityBehavior\EntityBehavior::class, // <-- Add this line
],
First Use Case: Soft Deletes Annotate an entity to enable soft deletes:
use Cycle\Annotated\Annotation\Column;
use Cycle\EntityBehavior\Annotation\SoftDelete;
#[SoftDelete]
class User
{
#[Column(type: 'primary')]
public int $id;
#[Column(type: 'string')]
public string $name;
#[Column(type: 'datetime', nullable: true)]
public ?string $deletedAt = null;
}
Now, User::delete() will set deletedAt instead of hard-deleting.
Soft Deletes
#[SoftDelete] to enable soft deletes.->whereDeletedAtIsNull() or ->withTrashed() (if using Cycle\ORM\Select\Query\Where\DeletedAt).->restore().Timestamps
#[Timestamp] to auto-manage createdAt/updatedAt:
#[Timestamp]
class Post
{
#[Column(type: 'datetime')]
public string $createdAt;
#[Column(type: 'datetime')]
public string $updatedAt;
}
Sluggable Fields
#[Sluggable] to auto-generate slugs:
#[Sluggable(['name'], 'slug')]
class Product
{
#[Column(type: 'string')]
public string $slug;
}
Observers
Cycle\EntityBehavior\Observer:
use Cycle\EntityBehavior\Observer;
class UserObserver extends Observer
{
public function beforeSave(User $user): void
{
$user->updatedAt = now();
}
}
config/cycle.php:
'observers' => [
User::class => UserObserver::class,
],
Optimistic Locking with Sequential Persist
#[OptimisticLock] works correctly with sequential persists (fixed in 1.7.1):
use Cycle\EntityBehavior\Annotation\OptimisticLock;
#[OptimisticLock]
class Product
{
#[Column(type: 'integer')]
public int $version = 1;
}
Query Builder Extensions
Use Cycle\EntityBehavior\Query\Where\DeletedAt for soft-delete queries:
$query->whereDeletedAtIsNull(); // Only active records
$query->whereDeletedAtIsNotNull(); // Only trashed records
Migrations
Ensure your migration matches the behavior’s expected schema (e.g., deletedAt column for soft deletes, version for optimistic locking).
Testing
Mock behaviors in tests by overriding methods or using Cycle\ORM\Select\Query\Where\DeletedAt directly.
For optimistic locking, test concurrent updates to verify version increments.
Schema Mismatch
deletedAt or version (for optimistic locking) is missing, behaviors will fail silently. Always include required columns in migrations.Observer Order
config/cycle.php.Sluggable Conflicts
#[Sluggable] fields are unique in the database to avoid collisions.Soft Delete Queries
->whereDeletedAtIsNull() will return both active and trashed records.Optimistic Locking Race Conditions
#[OptimisticLock] fails during concurrent updates, ensure your application handles Cycle\ORM\Exception\OptimisticLockException gracefully.Enable Logging
Add 'debug' => true to config/cycle.php to log behavior events.
Check Behavior Events
Use Cycle\EntityBehavior\Event to trace execution:
$event = new Cycle\EntityBehavior\Event\BeforeSave($entity);
$event->dispatch(); // Debug before dispatch
Optimistic Lock Debugging Enable SQL logging to verify version checks:
'debug' => [
'sql' => true,
],
Custom Behaviors
Extend Cycle\EntityBehavior\Behavior to create reusable logic:
class AuditBehavior extends Behavior
{
public function beforeSave(EntityInterface $entity): void
{
$entity->auditLog[] = new AuditLog();
}
}
Override Defaults
Modify behavior logic by extending annotations (e.g., #[SoftDelete(useCurrentTimestamp: false)]).
Query Extensions
Add custom Where clauses by implementing Cycle\ORM\Select\Query\Where\WhereInterface.
Behavior Priority
Later behaviors in config/cycle.php override earlier ones. Order matters for conflicts.
Annotation Caching
Clear vendor/compiled after adding new annotations to avoid stale behavior.
Optimistic Locking Fix (1.7.1)
The sequential persist issue with #[OptimisticLock] is now resolved. Ensure your tests cover concurrent update scenarios to validate the fix.
NO_UPDATE_NEEDED would not apply here due to the critical fix in 1.7.1. The assessment has been updated to reflect the new behavior and include testing/debugging tips for optimistic locking.
How can I help you explore Laravel packages today?