Pular para o conteúdo principal

Traits Reference

Traits provide reusable functionality for your models, particularly for automatic timestamp management and soft deletes.

Table of Contents

Overview

The reference architecture provides three timestamp traits that combine:

  • ORM Functionality: Automatic timestamp management from ByJG MicroOrm
  • OpenAPI Documentation: Automatic schema generation for timestamps

Location: src/Trait/

Available Traits

TraitPurposeField NameAuto-Updated
OaCreatedAtTrack record creation timecreated_atOn INSERT
OaUpdatedAtTrack record modification timeupdated_atOn INSERT/UPDATE
OaDeletedAtSoft delete supportdeleted_atManual

OaCreatedAt Trait

Automatically sets the creation timestamp when a record is inserted.

Location: src/Trait/OaCreatedAt.php

Usage

<?php

namespace RestReferenceArchitecture\Model;

use ByJG\MicroOrm\Attributes\TableAttribute;
use RestReferenceArchitecture\Trait\OaCreatedAt;
use OpenApi\Attributes as OA;

#[OA\Schema(required: ["id", "name"], type: "object")]
#[TableAttribute("products")]
class Product
{
use OaCreatedAt; // Adds created_at functionality

protected int $id;
protected string $name;

// Getters and setters...
}

What It Provides

// Property definition
protected string|null $createdAt = null;

// Getter
public function getCreatedAt(): ?string

// Setter
public function setCreatedAt(?string $value): static

// ORM Attribute
#[FieldAttribute(fieldName: "created_at", syncWithDb: false)]

// OpenAPI Attribute
#[OA\Property(type: "string", format: "date-time", nullable: true)]

Behavior

$product = new Product();
$product->setName('Widget');

// created_at is null
echo $product->getCreatedAt(); // null

$repository->save($product);

// created_at is automatically set
echo $product->getCreatedAt(); // "2024-01-15 10:30:45"

// Later updates don't change created_at
$product->setName('Updated Widget');
$repository->save($product);
echo $product->getCreatedAt(); // Still "2024-01-15 10:30:45"

OpenAPI Schema

When using this trait, the OpenAPI schema automatically includes:

Product:
type: object
properties:
created_at:
type: string
format: date-time
nullable: true

OaUpdatedAt Trait

Automatically updates the timestamp whenever a record is inserted or updated.

Location: src/Trait/OaUpdatedAt.php

Usage

<?php

namespace RestReferenceArchitecture\Model;

use RestReferenceArchitecture\Trait\OaUpdatedAt;

class Product
{
use OaUpdatedAt; // Adds updated_at functionality

// Other properties...
}

What It Provides

// Property definition
protected string|null $updatedAt = null;

// Getter
public function getUpdatedAt(): ?string

// Setter
public function setUpdatedAt(?string $value): static

// ORM Attribute
#[FieldAttribute(fieldName: "updated_at", syncWithDb: false)]

// OpenAPI Attribute
#[OA\Property(type: "string", format: "date-time", nullable: true)]

Behavior

$product = new Product();
$product->setName('Widget');

$repository->save($product);
echo $product->getUpdatedAt(); // "2024-01-15 10:30:45"

// Wait a moment...
sleep(2);

$product->setName('Updated Widget');
$repository->save($product);
echo $product->getUpdatedAt(); // "2024-01-15 10:30:47" (updated!)

OaDeletedAt Trait

Provides soft delete functionality by marking records as deleted instead of removing them from the database.

Location: src/Trait/OaDeletedAt.php

Usage

<?php

namespace RestReferenceArchitecture\Model;

use RestReferenceArchitecture\Trait\OaDeletedAt;

class Product
{
use OaDeletedAt; // Adds deleted_at functionality

// Other properties...
}

What It Provides

// Property definition
protected string|null $deletedAt = null;

// Getter
public function getDeletedAt(): ?string

// Setter
public function setDeletedAt(?string $value): static

// Check if deleted
public function isDeleted(): bool

Behavior

$product = $repository->get($id);
echo $product->getDeletedAt(); // null (not deleted)
echo $product->isDeleted(); // false

// Soft delete
$product->setDeletedAt(date('Y-m-d H:i:s'));
$repository->save($product);

echo $product->isDeleted(); // true

// Restore
$product->setDeletedAt(null);
$repository->save($product);
echo $product->isDeleted(); // false

Service Layer Integration

Implement soft delete in your service:

class ProductService extends BaseService
{
public function softDelete(int $id): void
{
$product = $this->getOrFail($id);
$product->setDeletedAt(date('Y-m-d H:i:s'));
$this->save($product);
}

public function restore(int $id): void
{
$product = $this->getOrFail($id);
$product->setDeletedAt(null);
$this->save($product);
}

/**
* List only non-deleted records
*/
public function list(?int $page = null, ?int $size = null): array
{
$query = $this->repository->listQuery(
page: $page,
size: $size,
filter: [
['deleted_at IS NULL', []]
]
);

return $this->repository->getRepository()->getByQuery($query);
}
}

Using Multiple Traits

Combine traits for complete timestamp tracking:

<?php

namespace RestReferenceArchitecture\Model;

use ByJG\MicroOrm\Attributes\FieldAttribute;
use ByJG\MicroOrm\Attributes\TableAttribute;
use OpenApi\Attributes as OA;
use RestReferenceArchitecture\Trait\OaCreatedAt;
use RestReferenceArchitecture\Trait\OaUpdatedAt;
use RestReferenceArchitecture\Trait\OaDeletedAt;

#[OA\Schema(required: ["id", "name"], type: "object")]
#[TableAttribute("products")]
class Product
{
use OaCreatedAt; // Track creation
use OaUpdatedAt; // Track updates
use OaDeletedAt; // Soft deletes

#[OA\Property(type: "integer", format: "int32")]
#[FieldAttribute(primaryKey: true, fieldName: "id")]
protected int|null $id = null;

#[OA\Property(type: "string")]
#[FieldAttribute(fieldName: "name")]
protected string $name;

// Getters and setters...
}

Complete Lifecycle Example

// CREATE
$product = new Product();
$product->setName('Widget');

$repository->save($product);

echo $product->getCreatedAt(); // "2024-01-15 10:30:45"
echo $product->getUpdatedAt(); // "2024-01-15 10:30:45"
echo $product->getDeletedAt(); // null

// UPDATE
sleep(2);
$product->setName('Updated Widget');
$repository->save($product);

echo $product->getCreatedAt(); // "2024-01-15 10:30:45" (unchanged)
echo $product->getUpdatedAt(); // "2024-01-15 10:30:47" (updated!)
echo $product->getDeletedAt(); // null

// SOFT DELETE
$product->setDeletedAt(date('Y-m-d H:i:s'));
$repository->save($product);

echo $product->getCreatedAt(); // "2024-01-15 10:30:45"
echo $product->getUpdatedAt(); // "2024-01-15 10:30:49" (updated by save)
echo $product->getDeletedAt(); // "2024-01-15 10:30:49" (marked deleted)
echo $product->isDeleted(); // true

Database Setup

Migration Example

Create database columns for timestamp fields:

CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price DECIMAL(10,2),
created_at DATETIME NULL,
updated_at DATETIME NULL,
deleted_at DATETIME NULL
);

Using Code Generator

The code generator automatically adds timestamp fields:

composer run gen create products name:string price:decimal

Generates:

class Product
{
use OaCreatedAt;
use OaUpdatedAt;
// Other fields...
}

Database Triggers (Alternative)

You can also use database triggers instead of application-level timestamps:

CREATE TRIGGER products_before_insert
BEFORE INSERT ON products
FOR EACH ROW
SET NEW.created_at = CURRENT_TIMESTAMP,
NEW.updated_at = CURRENT_TIMESTAMP;

CREATE TRIGGER products_before_update
BEFORE UPDATE ON products
FOR EACH ROW
SET NEW.updated_at = CURRENT_TIMESTAMP;

Note: When using database triggers, set syncWithDb: true in the FieldAttribute:

#[FieldAttribute(fieldName: "created_at", syncWithDb: true)]
protected string|null $createdAt = null;

Custom Timestamp Fields

Custom Field Names

If your database uses different column names:

use ByJG\MicroOrm\Trait\CreatedAt;
use ByJG\MicroOrm\Attributes\FieldAttribute;
use OpenApi\Attributes as OA;

trait CustomCreatedAt
{
use CreatedAt;

#[OA\Property(type: "string", format: "date-time", nullable: true)]
#[FieldAttribute(fieldName: "date_created", syncWithDb: false)]
protected string|null $createdAt = null;
}

Custom Timestamp Format

Override getters/setters for custom formats:

trait CustomCreatedAt
{
use OaCreatedAt;

public function getCreatedAt(): ?string
{
$timestamp = parent::getCreatedAt();
return $timestamp ? date('c', strtotime($timestamp)) : null; // ISO 8601
}

public function setCreatedAt(?string $value): static
{
if ($value) {
$value = date('Y-m-d H:i:s', strtotime($value));
}
return parent::setCreatedAt($value);
}
}

Additional Timestamp Fields

Create custom timestamp traits:

<?php

namespace RestReferenceArchitecture\Trait;

use ByJG\MicroOrm\Attributes\FieldAttribute;
use OpenApi\Attributes as OA;

trait OaPublishedAt
{
#[OA\Property(type: "string", format: "date-time", nullable: true)]
#[FieldAttribute(fieldName: "published_at", syncWithDb: false)]
protected string|null $publishedAt = null;

public function getPublishedAt(): ?string
{
return $this->publishedAt;
}

public function setPublishedAt(?string $value): static
{
$this->publishedAt = $value;
return $this;
}

public function isPublished(): bool
{
return !empty($this->publishedAt) &&
strtotime($this->publishedAt) <= time();
}

public function publish(): static
{
$this->publishedAt = date('Y-m-d H:i:s');
return $this;
}

public function unpublish(): static
{
$this->publishedAt = null;
return $this;
}
}

Best Practices

1. Always Use Timestamp Traits

// Good - Automatic tracking
class Product
{
use OaCreatedAt;
use OaUpdatedAt;
}

// Bad - Manual tracking (error-prone)
class Product
{
protected string $createdAt;

public function setCreatedAt(string $value) {
$this->createdAt = $value;
}
}

2. Use OaDeletedAt for Audit Trail

// Good - Keep deleted records for audit
class Order
{
use OaDeletedAt;
}

// Bad - Permanent deletion loses data
$repository->delete($id);

3. Filter Soft-Deleted Records

// Good - Exclude deleted records by default
public function list($page, $size): array
{
return $this->repository->list(
page: $page,
size: $size,
filter: [['deleted_at IS NULL', []]]
);
}

// Bad - Returns deleted records
public function list($page, $size): array
{
return $this->repository->list($page, $size);
}

4. Set Nullable Constraints

-- Good - Allow NULL for optional timestamps
created_at DATETIME NULL,
updated_at DATETIME NULL,
deleted_at DATETIME NULL

-- Bad - NOT NULL requires manual management
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP

5. Combine with ActiveRecord

class Product
{
use ActiveRecord;
use OaCreatedAt;
use OaUpdatedAt;
use OaDeletedAt;

// Custom query for non-deleted records
public static function findActive(): ?array
{
$query = Query::getInstance()
->table(self::$repository->getMapper()->getTable())
->where('deleted_at IS NULL');
return self::query($query);
}
}

6. Document Timestamp Behavior

/**
* Product Model
*
* Timestamps:
* - created_at: Set automatically on first save
* - updated_at: Updated automatically on every save
* - deleted_at: Set manually for soft deletes
*/
class Product
{
use OaCreatedAt;
use OaUpdatedAt;
use OaDeletedAt;
}

Troubleshooting

Timestamps Not Updating

Problem: updated_at not changing on save

Solution: Ensure syncWithDb: false in FieldAttribute:

#[FieldAttribute(fieldName: "updated_at", syncWithDb: false)]

OpenAPI Generation Error

Problem: Error when running composer run openapi on traits

Solution: This is expected if traits are not used in any model. The error only occurs when traits exist but aren't used. Once you use a trait in a model, OpenAPI generation works correctly.

Timezone Issues

Problem: Timestamps in wrong timezone

Solution: Set timezone in config/02-security/01-timezone.php:

return [
'timezone' => fn() => date_default_timezone_set('UTC')
];

NULL Timestamps

Problem: All timestamps are NULL

Solution: Check database column allows NULL and ORM mapping is correct:

SHOW COLUMNS FROM products LIKE '%_at';