Skip to content

Behavioral Traits

lpachecob edited this page Mar 24, 2026 · 6 revisions

πŸ›‘οΈ Behavioral Attributes: Enterprise Features

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.


πŸ“ Location

Infrastructure\FrameworkCore\Attributes\Behavior\

πŸ›‘οΈ #[Auditable]

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"
}

⏰ #[Timestampable]

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_at is automatically set on INSERT
  • updated_at is automatically updated on UPDATE
  • Uses database TIMESTAMP with automatic default

πŸ‘€ #[Blameable]

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])

πŸ—‘οΈ #[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:

  1. Database: core:migrate adds deleted_at column
  2. Queries: All GET requests automatically filter WHERE deleted_at IS NULL
  3. DELETE Action: Sets the timestamp instead of executing SQL DELETE
  4. 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"}

🏒 #[TenantAware]

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:

  1. Database: core:migrate adds the tenant column
  2. Queries: Every SELECT, INSERT, UPDATE, and DELETE is automatically scoped
  3. Zero Configuration: Pass X-Tenant-ID header 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'

πŸ” #[Authorize]

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 { ... }

πŸ›‘οΈ #[Policy]

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

πŸ”€ #[Sluggable]

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"
}

⚑ #[RateLimit]

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:

  1. Per-IP by default: Limits requests from each IP address
  2. Per-user when authenticated: Uses user ID instead of IP
  3. Per-resource: Each entity can have different limits
  4. Headers included: Response includes rate limit headers

Response Headers:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45

When 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=1

πŸ”’ #[ThrottleLogin]

Configures 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
    }
}

πŸ” #[Ownership]

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');
}

πŸ”„ #[Transactional]

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:

  1. All INSERT, UPDATE, and DELETE operations are wrapped in a transaction
  2. If any operation fails, all changes are rolled back
  3. Supports configurable retry attempts for transient failures
  4. Nested transactions can be disabled for performance

πŸ“‹ Combining Behavioral Attributes

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 πŸ”’

Clone this wiki locally