-
Notifications
You must be signed in to change notification settings - Fork 0
Behavioral Traits
In BOUNDLY, behavioral attributes add enterprise-level features with a single declaration. These automate horizontal concerns like auditing, deletion, multi-tenancy, authorization, and automatic data transformations.
Infrastructure\FrameworkCore\Attributes\Behavior\Automates traceability by tracking WHO created and modified each record.
| Column Added | Source |
|---|---|
created_by |
X-User-ID request header (or 'System') |
updated_by |
X-User-ID request header (or 'System') |
Use Case: Compliance requirements, audit trails, knowing who touched what data.
Example:
use Infrastructure\FrameworkCore\Attributes\Schema\{Entity, Column, Id};
use Infrastructure\FrameworkCore\Attributes\Behavior\Auditable;
#[Entity(table: 'products', resource: 'products')]
#[Auditable]
class Product extends AggregateRoot
{
#[Id]
private int $id;
#[Column(type: 'string', length: 150)]
private string $name;
#[Column(type: 'decimal(10,2)')]
private string $price;
}Generated Schema:
ALTER TABLE products ADD COLUMN created_by VARCHAR(255) NULL;
ALTER TABLE products ADD COLUMN updated_by VARCHAR(255) NULL;API Behavior:
// POST /api/products (with X-User-ID: admin@company.com)
{
"name": "Premium Widget",
"price": 99.99
}
// Response includes audit trail
{
"id": 1,
"name": "Premium Widget",
"price": 99.99,
"created_by": "admin@company.com",
"updated_by": "admin@company.com",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}Automatically manages created_at and updated_at timestamps.
| Parameter | Type | Default | Description |
|---|---|---|---|
createdAt |
string |
'created_at' |
Column name for creation time |
updatedAt |
string |
'updated_at' |
Column name for last update time |
Use Case: Standard timestamp tracking without manual management.
Example:
use Infrastructure\FrameworkCore\Attributes\Schema\{Entity, Column, Id};
use Infrastructure\FrameworkCore\Attributes\Behavior\Timestampable;
#[Entity(table: 'articles', resource: 'articles')]
#[Timestampable(createdAt: 'published_at', updatedAt: 'last_modified_at')]
class Article extends AggregateRoot
{
#[Id]
private int $id;
#[Column(type: 'string', length: 255)]
private string $title;
#[Column(type: 'text')]
private string $content;
}Behavior:
-
created_atis automatically set on INSERT -
updated_atis automatically updated on UPDATE - Uses database TIMESTAMP with automatic default
Extended audit trail that tracks WHO performed each operation, including deletions.
| Parameter | Type | Default | Description |
|---|---|---|---|
createdBy |
string |
'created_by' |
Column for creator user ID |
updatedBy |
string |
'updated_by' |
Column for last modifier user ID |
deletedBy |
string |
'deleted_by' |
Column for deleter user ID |
Use Case: GDPR compliance, complete audit trails, knowing who deleted what.
Example:
use Infrastructure\FrameworkCore\Attributes\Schema\{Entity, Column, Id};
use Infrastructure\FrameworkCore\Attributes\Behavior\{Blameable, SoftDelete};
#[Entity(table: 'contracts', resource: 'contracts')]
#[Blameable]
#[SoftDelete]
class Contract extends AggregateRoot
{
#[Id]
private int $id;
#[Column(type: 'string', length: 150)]
private string $title;
#[Column(type: 'decimal(12,2)')]
private string $value;
}Generated Schema:
ALTER TABLE contracts ADD COLUMN created_by VARCHAR(255) NULL;
ALTER TABLE contracts ADD COLUMN updated_by VARCHAR(255) NULL;
ALTER TABLE contracts ADD COLUMN deleted_by VARCHAR(255) NULL;
ALTER TABLE contracts ADD COLUMN deleted_at TIMESTAMP NULL;API Behavior:
- Tracks who created each record
- Tracks who last modified each record
- Tracks who deleted each record (when combined with
#[SoftDelete])
Enables logical (soft) deletion β records are never physically removed from the database.
| Column Added | Behavior |
|---|---|
deleted_at |
TIMESTAMP - NULL means active, timestamp means deleted |
Use Case: Data recovery, audit compliance, preventing accidental data loss.
Example:
use Infrastructure\FrameworkCore\Attributes\Schema\{Entity, Column, Id};
use Infrastructure\FrameworkCore\Attributes\Behavior\SoftDelete;
#[Entity(table: 'orders', resource: 'orders')]
#[SoftDelete]
class Order extends AggregateRoot
{
#[Id]
private int $id;
#[Column(type: 'string', length: 50)]
private string $orderNumber;
#[Column(type: 'decimal(10,2)')]
private string $total;
}Behavior:
-
Database:
core:migrateaddsdeleted_atcolumn -
Queries: All
GETrequests automatically filterWHERE deleted_at IS NULL -
DELETE Action: Sets the timestamp instead of executing SQL
DELETE -
Recovery: Records can be restored by setting
deleted_at = NULL
API Behavior:
# List active orders (deleted_at IS NULL)
GET /api/orders
# Returns only non-deleted orders
# Delete an order (sets deleted_at, doesn't remove row)
DELETE /api/orders/123
# Response: {"status": "success", "deleted_at": "2024-01-15T14:30:00Z"}Isolates data by Tenant ID. Essential for SaaS multi-tenant applications.
| Parameter | Type | Default | Description |
|---|---|---|---|
tenantColumn |
string |
'tenant_id' |
Column name for tenant identifier |
Use Case: Multi-tenant SaaS applications where each tenant must only see their own data.
Example:
use Infrastructure\FrameworkCore\Attributes\Schema\{Entity, Column, Id};
use Infrastructure\FrameworkCore\Attributes\Behavior\TenantAware;
#[Entity(table: 'invoices', resource: 'invoices')]
#[TenantAware(tenantColumn: 'tenant_id')]
class Invoice extends AggregateRoot
{
#[Id]
private int $id;
#[Column(type: 'string', length: 50)]
private string $invoiceNumber;
#[Column(type: 'decimal(10,2)')]
private string $amount;
}Behavior:
-
Database:
core:migrateadds the tenant column -
Queries: Every
SELECT,INSERT,UPDATE, andDELETEis automatically scoped -
Zero Configuration: Pass
X-Tenant-IDheader in API requests
API Usage:
# Request with tenant header
curl -X GET /api/invoices \
-H "X-Tenant-ID: tenant_abc123" \
-H "Authorization: Bearer token"
# All queries automatically include:
# WHERE tenant_id = 'tenant_abc123'Protects an entity's API routes with authentication and role-based access control (RBAC).
| Parameter | Type | Default | Description |
|---|---|---|---|
roles |
array |
[] |
Required role names. Empty = any authenticated user |
methods |
array |
[] |
HTTP methods this rule applies to. Empty = all |
guard |
string |
'sanctum' |
Laravel auth guard to use |
Use Case: Protecting API endpoints based on user roles and authentication status.
Example:
// Any authenticated user can access
#[Entity(table: 'reports', resource: 'reports')]
#[Authorize]
class Report extends AggregateRoot { ... }
// Only admins can access
#[Entity(table: 'salaries', resource: 'salaries')]
#[Authorize(roles: ['admin'])]
class Salary extends AggregateRoot { ... }
// Public reads, authenticated writes
#[Entity(table: 'articles', resource: 'articles')]
#[Authorize(roles: [], methods: ['POST', 'PUT', 'PATCH', 'DELETE'])]
class Article extends AggregateRoot { ... }
// Multiple roles (any of these)
#[Entity(table: 'settings', resource: 'settings')]
#[Authorize(roles: ['admin', 'super-admin'])]
class Setting extends AggregateRoot { ... }
// Multiple Authorize rules (stacked)
#[Entity(table: 'dashboard', resource: 'dashboard')]
#[Authorize(roles: ['viewer'], methods: ['GET'])]
#[Authorize(roles: ['editor'], methods: ['POST', 'PUT'])]
#[Authorize(roles: ['admin'], methods: ['DELETE'])]
class Dashboard extends AggregateRoot { ... }Maps a Laravel Policy for fine-grained authorization control using policy methods.
| Parameter | Type | Default | Description |
|---|---|---|---|
class |
string |
null |
Policy class name |
methods |
array |
[] |
Verb-to-method mapping |
Use Case: Complex authorization logic that goes beyond simple role checking.
Example:
use Infrastructure\FrameworkCore\Attributes\Schema\{Entity, Column, Id};
use Infrastructure\FrameworkCore\Attributes\Behavior\Policy;
#[Entity(table: 'posts', resource: 'posts')]
#[Policy(PostPolicy::class)]
class Post extends AggregateRoot
{
#[Id]
private int $id;
#[Column(type: 'string', length: 255)]
private string $title;
#[Column(type: 'boolean', default: false)]
private bool $isPublished;
}Policy Class:
class PostPolicy
{
public function viewAny(User $user): bool
{
return true; // Everyone can view posts
}
public function view(User $user, Post $post): bool
{
return $user->id === $post->user_id || $post->isPublished;
}
public function create(User $user): bool
{
return $user->can_create_posts;
}
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id || $user->isAdmin();
}
public function delete(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
}Verb-to-Method Mapping:
| HTTP Verb | Resource | Policy Method |
|---|---|---|
GET |
/posts |
viewAny |
GET |
/posts/{id} |
view |
POST |
/posts |
create |
PUT/PATCH |
/posts/{id} |
update |
DELETE |
/posts/{id} |
delete |
Automatically generates URL-friendly slugs from another field.
| Parameter | Type | Default | Description |
|---|---|---|---|
source |
string |
Required | Property name to generate slug from |
target |
string |
'slug' |
Property name to store generated slug |
unique |
bool |
true |
Append numeric suffix if slug already exists |
Use Case: Creating SEO-friendly URLs from titles or names.
Example:
use Infrastructure\FrameworkCore\Attributes\Schema\{Entity, Column, Id};
use Infrastructure\FrameworkCore\Attributes\Behavior\Sluggable;
#[Entity(table: 'categories', resource: 'categories')]
class Category extends AggregateRoot
{
#[Id]
private int $id;
#[Column(type: 'string', length: 150)]
private string $name;
#[Sluggable(source: 'name', target: 'slug', unique: true)]
#[Column(type: 'string', length: 200, unique: true)]
private string $slug;
}Behavior:
| Input | Output |
|---|---|
"Hello World!" |
hello-world |
"Python 3.12 Released" |
python-312-released |
"News & Updates" |
news-updates |
"Category" (if exists) |
category-1 |
"Category" (if exists) |
category-2 |
API Example:
// POST /api/categories
{
"name": "Breaking News Today!"
}
// Automatically generated
{
"id": 1,
"name": "Breaking News Today!",
"slug": "breaking-news-today"
}Protects API endpoints from abuse by limiting the number of requests per user/IP.
| Parameter | Type | Default | Description |
|---|---|---|---|
maxAttempts |
int |
60 |
Maximum requests allowed in the time window |
decayMinutes |
int |
1 |
Time window in minutes before attempts reset |
prefix |
string |
null |
Custom rate limit key prefix |
Use Case: Preventing API abuse, brute-force protection, ensuring fair usage.
Example:
// Global rate limit for all API requests (config/boundly.php)
'rate_limit' => [
'enabled' => true,
'max_attempts' => 60, // 60 requests
'decay_minutes' => 1, // per minute
],
// Entity-specific rate limit
#[Entity(table: 'auth', resource: 'auth')]
#[RateLimit(maxAttempts: 5, decayMinutes: 1)]
class Authentication extends AggregateRoot { ... }
// Sensitive endpoint with stricter limits
#[Entity(table: 'payments', resource: 'payments')]
#[RateLimit(maxAttempts: 10, decayMinutes: 1)]
class Payment extends AggregateRoot { ... }
// Public read-heavy endpoint with higher limits
#[Entity(table: 'products', resource: 'products')]
#[RateLimit(maxAttempts: 120, decayMinutes: 1)]
class Product extends AggregateRoot { ... }Behavior:
- Per-IP by default: Limits requests from each IP address
- Per-user when authenticated: Uses user ID instead of IP
- Per-resource: Each entity can have different limits
- Headers included: Response includes rate limit headers
Response Headers:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45When Exceeded (HTTP 429):
{
"status": "error",
"message": "Too many requests. Please slow down and try again later.",
"error": {
"max_attempts": 60,
"retry_after": 45
}
}Environment Configuration:
# Disable rate limiting
BOUNDLY_RATE_LIMIT_ENABLED=false
# Global defaults
BOUNDLY_RATE_LIMIT_MAX_ATTEMPTS=60
BOUNDLY_RATE_LIMIT_DECAY_MINUTES=1Configures login attempt throttling to protect against brute force attacks.
| Parameter | Type | Default | Description |
|---|---|---|---|
maxAttempts |
int |
5 |
Maximum failed attempts before lockout |
decayMinutes |
int |
15 |
Minutes before attempts reset |
trackBy |
string |
'email' |
Field to track (email, username) |
lockoutEnabled |
bool |
true |
Enable progressive lockout |
lockoutMultiplier |
int |
2 |
Lockout duration multiplier |
maxLockouts |
int |
3 |
Maximum lockout count |
Use Case: Login endpoints, authentication endpoints that need brute force protection.
Example:
use Infrastructure\FrameworkCore\Attributes\Behavior\ThrottleLogin;
#[Entity(table: 'users', resource: 'users')]
class User extends AggregateRoot
{
#[ThrottleLogin(maxAttempts: 3, decayMinutes: 10)]
public function login(): void
{
// Login logic
}
}Defines object-level authorization rules to prevent BOLA/IDOR attacks.
| Parameter | Type | Default | Description |
|---|---|---|---|
ownerField |
string |
'user_id' |
Property containing the owner ID |
allowAdminBypass |
bool |
true |
Allow admin users to bypass |
resourceField |
string |
null |
Custom resource identifier |
Use Case: Ensuring users can only access their own resources.
Example:
use Infrastructure\FrameworkCore\Attributes\Behavior\Ownership;
#[Entity(table: 'documents', resource: 'documents')]
#[Ownership(ownerField: 'owner_id', allowAdminBypass: true)]
class Document extends AggregateRoot
{
#[Id]
private int $id;
#[Column(type: 'string', length: 255)]
private string $title;
#[Column(type: 'integer')]
private int $ownerId;
}OwnershipValidator Usage:
$validator = app(OwnershipValidator::class);
if (!$validator->canAccess($user, $document)) {
throw new AuthorizationException('Access denied');
}Ensures that all database operations for an entity are wrapped in a transaction.
| Parameter | Type | Default | Description |
|---|---|---|---|
tries |
int |
1 |
Number of retry attempts |
timeout |
int |
60 |
Transaction timeout in seconds |
nested |
bool |
false |
Allow nested transactions |
Use Case: Ensuring data consistency across multiple operations.
Example:
use Infrastructure\FrameworkCore\Attributes\Schema\{Entity, Column, Id};
use Infrastructure\FrameworkCore\Attributes\Behavior\Transactional;
#[Entity(table: 'orders', resource: 'orders')]
#[Transactional(tries: 3, timeout: 30)]
class Order extends AggregateRoot
{
#[Id]
private int $id;
#[Column(type: 'string', length: 50)]
private string $orderNumber;
#[Column(type: 'decimal(10,2)')]
private string $total;
}Behavior:
- All
INSERT,UPDATE, andDELETEoperations are wrapped in a transaction - If any operation fails, all changes are rolled back
- Supports configurable retry attempts for transient failures
- Nested transactions can be disabled for performance
Create truly enterprise-level entities by combining multiple behavioral attributes:
use Infrastructure\FrameworkCore\Attributes\Schema\{Entity, Column, Id};
use Infrastructure\FrameworkCore\Attributes\Behavior\{
Auditable,
Blameable,
SoftDelete,
TenantAware,
Authorize,
Timestampable,
Sluggable,
Policy
};
#[Entity(table: 'projects', resource: 'projects')]
#[Auditable]
#[Blameable]
#[SoftDelete]
#[TenantAware(tenantColumn: 'tenant_id')]
#[Timestampable]
#[Authorize(roles: ['admin', 'manager', 'member'])]
#[Policy(ProjectPolicy::class)]
class Project extends AggregateRoot
{
#[Id]
private int $id;
#[Sluggable(source: 'name', target: 'slug')]
#[Column(type: 'string', length: 255)]
private string $name;
#[Column(type: 'text')]
private string $description;
#[Column(type: 'date')]
private string $deadline;
#[Column(type: 'enum', length: 50)]
private string $status;
}With this declaration, the projects resource:
- β Auto-creates and evolves its table
- β Tracks who created and modified each record
- β Tracks who deleted each record
- β Is isolated per tenant
- β Has automatic timestamps
- β Generates SEO-friendly slugs
- β Is accessible only to authenticated users with specific roles
- β Uses policy-based authorization for complex rules
Next Step: Security-Attributes π