1. Go to this page and download the library: Download nandan108/attrecord library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
nandan108 / attrecord example snippets
use Nandan108\Attrecord\Record;
use Nandan108\Attrecord\Attribute\{Table, Column, Relation};
use Nandan108\Attrecord\Caster\EnumCaster;
use Nandan108\Attrecord\Enum\{ColumnType, RelationType};
enum OrderStatus: string
{
case Draft = 'draft';
case Placed = 'placed';
case Shipped = 'shipped';
}
#[Table(name: 'orders')]
class Order extends Record
{
#[Column(ColumnType::BigIntUnsigned, autoIncrement: true)]
public ?int $id = null;
#[Column(ColumnType::Enum)] // ENUM(...) value set derived from OrderStatus' cases
#[EnumCaster(OrderStatus::class)]
public OrderStatus $status = OrderStatus::Draft;
#[Column(ColumnType::Decimal, precision: 10, scale: 2, nullable: true)]
public ?float $total = null;
#[Column(ColumnType::DateTime, nullable: true)]
public ?\DateTimeImmutable $placed_at = null;
/** @var RecordSet<OrderLine>|null */
#[Relation(RelationType::OneToMany, class: OrderLine::class, foreignKey: 'order_id')]
public ?\Nandan108\Attrecord\RecordSet $lines = null;
}
#[Table(name: 'order_lines')]
class OrderLine extends Record
{
#[Column(ColumnType::BigIntUnsigned, autoIncrement: true)]
public ?int $id = null;
#[Column(ColumnType::BigIntUnsigned)]
public int $order_id = 0;
#[Column(ColumnType::VarChar, length: 200)]
public string $sku = '';
#[Column(ColumnType::IntUnsigned)]
public int $qty = 1;
#[Relation(RelationType::ManyToOne, class: Order::class, foreignKey: 'order_id')]
public ?Order $order = null;
}
use Nandan108\Attrecord\Connection;
use Nandan108\Attrecord\Dialect\MysqlDialect;
use Nandan108\Attrecord\Session\PdoDbSession;
$pdo = new PDO('mysql:host=127.0.0.1;dbname=shop;charset=utf8mb4', 'user', 'pass');
$conn = new Connection(new PdoDbSession($pdo), new MysqlDialect());
Record::setConnection($conn);
Record::setTablePrefix('wp_'); // Order → `wp_orders`, OrderLine → `wp_order_lines`
// INSERT — classic style
$order = new Order();
$order->status = 'pending';
$order->total = 99.95;
$order->save(); // INSERT INTO `orders` …
echo $order->id; // auto-assigned PK
// INSERT — fluent factory style
$order = Order::newWith(['status' => 'pending', 'total' => 99.95])->save();
echo $order->id; // auto-assigned PK
// Bulk-assign on an existing instance
$order->set(['status' => 'confirmed', 'total' => 149.00])->save();
// set() calls validate() by default — pass false to defer validation
// (useful for test fixtures or staged construction across multiple set() calls).
// save() / upsertAll() will still validate at the boundary.
$order->set(['status' => 'confirmed'], validate: false);
// save() always returns $this — check $_saved if you need to know whether a write occurred
$order->save();
$order->_saved; // true = INSERT or UPDATE was issued
// false = record was clean, nothing sent to DB
// null = save() not yet called on this instance
// SELECT by PK
$order = Order::getOne(42); // ?Order
$order = Order::getOneOrFail(42); // Order (throws RecordNotFoundException if missing)
$order = Order::getOneOrNew(42); // Order (new, unsaved instance if missing)
// UPDATE — only dirty columns
$order->status = 'confirmed';
$order->save(); // UPDATE `orders` SET `status` = ? WHERE `id` = ?
// DELETE
$order->delete();
// Reload from DB (e.g. after an external update)
$order->reload();
// All records (no WHERE)
$all = Order::find();
// With WHERE clause — positional params
$pending = Order::find('`status` = ?', ['pending']);
// With WHERE clause — named params
$recent = Order::find('`placed_at` > :since', ['since' => '2024-01-01']);
// ORDER BY / LIMIT
$top10 = Order::find('`total` > ?', [100], 'ORDER BY `total` DESC LIMIT 10');
// First match or null
$draft = Order::findOne('`status` = ?', ['draft']);
// findOne accepts ORDER BY and FOR UPDATE too
$latestPending = Order::findOne(
'`status` = ?',
['pending'],
orderByLimit: 'ORDER BY `placed_at` DESC',
forUpdate: true, // inside transactional() only
);
// Count
$count = Order::countWhere('`status` = ?', ['pending']);
// Bulk update — column → value map; values are typed via the column's serializer
$updated = Order::updateWhere(
['status' => 'archived'],
'`status` = ? AND `placed_at` < ?',
['draft', '2024-01-01'],
);
// Bulk delete
$deleted = Order::deleteWhere('`status` = ? AND `total` IS NULL', ['draft']);
use Nandan108\Attrecord\RawSql;
// Increment a counter — no params needed
Order::updateWhere(
['view_count' => new RawSql('`view_count` + 1')],
'`id` = ?', [$id],
);
// Conditional bulk write
Order::updateWhere(
['priority' => new RawSql('CASE WHEN `total` > 500 THEN 1 ELSE 0 END')],
'`status` = ?', ['pending'],
);
// Parameterised raw expression — RawSql params come BEFORE the WHERE params
Order::updateWhere(
['priority' => new RawSql('GREATEST(?, `priority`)', [5])],
'`status` = ?', ['pending'],
);
$jsonHas = new RawSql('JSON_CONTAINS(`tags`, ?)', ['"featured"']);
Order::find(WhereClause::whereRaw($jsonHas));
// ...
Order::updateWhere(
['featured_at' => new RawSql('NOW()')],
WhereClause::whereRaw($jsonHas),
);
// Single-column equality
$pending = Order::where('status', 'pending');
// Comparison operator
$large = Order::where('total', 100, '>');
// NULL check (null value → IS NULL / IS NOT NULL)
$unplaced = Order::where('placed_at', null);
// IN list
$active = Order::whereIn('status', ['pending', 'confirmed']);
use Nandan108\Attrecord\WhereClause as WC;
$clause = WC::where('status', 'pending')
->andWhere(
WC::where('total', 100, '>')
->orWhere(WC::where('flagged', true))
);
$orders = Order::find($clause);
use Nandan108\Attrecord\Attribute\{Column, UniqueKey, Index};
#[Table(name: 'inventory_items')]
class InventoryItem extends Record
{
#[Column(ColumnType::BigIntUnsigned, autoIncrement: true)]
public ?int $id = null;
// Single-column unique key
#[Column(ColumnType::VarChar, length: 64)]
#[UniqueKey('sku')]
public string $sku = '';
// Compound unique key: (location_id, bin) — same name on both columns,
// composite ordering follows property declaration order
#[Column(ColumnType::BigIntUnsigned)]
#[UniqueKey('loc_bin')]
public int $location_id = 0;
#[Column(ColumnType::VarChar, length: 32)]
#[UniqueKey('loc_bin')]
public string $bin = '';
// Single-column secondary index
#[Column(ColumnType::IntUnsigned)]
#[Index('idx_qty')]
public int $qty = 0;
}
#[Table(name: 'inventory_items')]
#[UniqueKey('uk_loc_bin', columns: ['location_id', 'bin'])]
#[Index ('idx_loc_qty', columns: ['location_id', 'qty'])]
class InventoryItem extends Record { /* ... */ }
$item = new InventoryItem();
$item->sku = 'WIDGET-1';
$item->location_id = 1;
$item->bin = 'A-01';
$item->qty = 10;
// Insert if new; on SKU conflict, only overwrite qty
$item->upsertByUniqueKey('sku', updateColumns: ['qty']);
// Re-registering the same SKU never advances the auto-increment counter
$item->upsertByUniqueKey('sku', updateColumns: ['qty'], preserveAutoIncrement: true);
$plugin = PluginPolicy::newWith(['plugin_slug' => $slug, 'plugin_name' => $name]);
$pName = PluginPolicy::upsertCol('plugin_name'); // ->name (raw) / ->incoming / ->stored
$plugin->upsertByUniqueKey('uk_slug', [
// keep the stored display name unless a non-empty new one arrives
...$pName->setRaw(
"CASE WHEN {$pName->incoming} <> ? THEN {$pName->incoming} ELSE {$pName->stored} END",
[''], // the '' comparison, bound not inlined
),
'last_seen_at' => new RawSql('CURRENT_TIMESTAMP(6)'), // refresh to the DB clock
'policy', // plain entry — overwrite with the incoming value
]);
$item = new InventoryItem();
$item->sku = 'WIDGET-1'; // matches the 'sku' unique key
$item->qty = 25;
// UPDATE inventory_items SET qty = 25 WHERE sku = 'WIDGET-1'
$affected = $item->updateByUniqueKey();
// Restrict / allow nulls explicitly
$item->updateByUniqueKey(fields: ['qty', 'notes']);
use Nandan108\Attrecord\Dialect\MysqlDialect;
use Nandan108\Attrecord\Schema\TableSchema;
$sql = (new MysqlDialect())->buildCreateTable(
TableSchema::fromClass(Order::class),
);
// CREATE TABLE `orders` (
// `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
// `customer_id` BIGINT UNSIGNED NOT NULL,
// `status` VARCHAR(20) NOT NULL DEFAULT 'pending',
// `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
// `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
// ON UPDATE CURRENT_TIMESTAMP,
// PRIMARY KEY (`id`),
// UNIQUE KEY `uk_external` (`external_ref`),
// KEY `idx_status_date` (`status`, `created_at`),
// CONSTRAINT `fk_orders_customer_id` FOREIGN KEY (`customer_id`)
// REFERENCES `customers` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
// ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
use Nandan108\Attrecord\Dialect\PgsqlDialect;
$sql = (new PgsqlDialect())->buildCreateTable(TableSchema::fromClass(Order::class));
// CREATE TABLE "orders" (
// "id" BIGSERIAL,
// "customer_id" BIGINT NOT NULL,
// "status" VARCHAR(20) NOT NULL DEFAULT 'pending',
// ...
// PRIMARY KEY ("id"),
// CONSTRAINT "uk_external" UNIQUE ("external_ref"),
// CONSTRAINT "fk_orders_customer_id" FOREIGN KEY ("customer_id")
// REFERENCES "customers" ("id") ON DELETE CASCADE ON UPDATE RESTRICT
// );
// CREATE INDEX "idx_status_date" ON "orders" ("status", "created_at")
use Nandan108\Attrecord\Dialect\SqliteDialect;
$sql = (new SqliteDialect())->buildCreateTable(TableSchema::fromClass(Order::class));
// CREATE TABLE "orders" (
// "id" INTEGER PRIMARY KEY AUTOINCREMENT,
// "customer_id" INTEGER NOT NULL,
// "status" TEXT NOT NULL DEFAULT 'pending',
// ...
// CONSTRAINT "uk_external" UNIQUE ("external_ref"),
// CONSTRAINT "fk_orders_customer_id" FOREIGN KEY ("customer_id")
// REFERENCES "customers" ("id") ON DELETE CASCADE ON UPDATE RESTRICT
// );
// CREATE INDEX "idx_status_date" ON "orders" ("status", "created_at")
use Nandan108\Attrecord\Attribute\{Table, Column, ForeignKey};
use Nandan108\Attrecord\Enum\ForeignKeyAction;
#[Table(name: 'inventory_ledger')]
#[ForeignKey(column: 'subject_id', references: Subject::class)] // → `subjects`(`id`), derived
#[ForeignKey(column: 'from_slot_id', references: 'slotspace', referencesColumn: 'id', // → raw table, no Record
onDelete: ForeignKeyAction::SetNull)]
final class InventoryLedger extends Record
{
#[Column(ColumnType::BigIntUnsigned)]
public int $subject_id = 0;
#[Column(ColumnType::BigIntUnsigned, nullable: true)]
public ?int $from_slot_id = null;
// ...
}
use Nandan108\Attrecord\Attribute\Column;
use Nandan108\Attrecord\Enum\ColumnType;
use Nandan108\Attrecord\Enum\GeneratedColumnMode;
#[Column(
type: ColumnType::IntUnsigned,
generatedAs: 'IFNULL(scope_actor_id, 0)',
generatedMode: GeneratedColumnMode::Stored, // or Virtual; defaults to Stored
)]
public int $scope_actor_key = 0;
$proto = new InventoryItem();
$proto->qty = 0;
// Zero out qty for every item at a given location
$proto->updateByWhere('`location_id` = ?', [$locationId]);
// array ⇄ JSON — auto-attached on a Json column typed as array
#[Column(ColumnType::Json, nullable: true)]
public ?array $meta = null;
// value object ⇄ JSON — auto-attached when the type implements JsonCastable
#[Column(ColumnType::Json, nullable: true)]
public ?Money $price = null;
// explicit, parameterized caster
#[Column(ColumnType::Json, nullable: true)]
#[JsonCaster(excludeNullFields: ['note'])]
public ?array $audit = null;
// reshape a native type — e.g. store a timestamp as a unix int
#[Column(ColumnType::BigIntUnsigned, nullable: true)]
#[EpochCaster]
public ?\DateTimeImmutable $logged_at = null;
// flag-set ⇄ integer bitmask — portable across all three backends
#[Column(ColumnType::BigIntUnsigned, default: 0)]
#[BitmaskCaster(StockConcern::class)] // int-backed, power-of-two enum
public array $concerns = []; // e.g. [StockConcern::Deficit, StockConcern::NoCost]
use Nandan108\Attrecord\Exception\RecordValidationException;
class Order extends Record
{
// ... columns ...
public function validate(): void
{
if ($this->total !== null && $this->total < 0) {
throw new RecordValidationException(
'Order total cannot be negative.',
context: ['total' => $this->total],
);
}
if ($this->status === 'shipped' && $this->placed_at === null) {
throw new RecordValidationException('Shipped order must have a placed_at.');
}
}
}
$orders = Order::find('`status` = ?', ['pending']);
count($orders); // Countable
foreach ($orders as $o) {} // Iterator
$orders->first(); // ?Order
$orders->last(); // ?Order
// Extract one field, keyed by the PK
$idToTotal = $orders->pluck('total'); // [pk => total]
// Extract multiple fields, keyed by the PK
$details = $orders->pluck(['status', 'total']); // [pk => ['status' => …, 'total' => …]]
// Group + extract — extra args are the grouping key(s); leaves are field values
$byStatusTotals = $orders->pluck('total', 'status');
// ['pending' => [10.0, 25.5, …], 'confirmed' => [99.95, …]]
$byStatusDetails = $orders->pluck(['id', 'total'], 'status');
// ['pending' => [['id' => 1, 'total' => 10.0], …], …]
// Index by a unique column
$byId = $orders->recordsByKey('id'); // array<int|string, Order>
// Group by a single column
$byStatus = $orders->recordsGroupedByKey('status'); // array<string, RecordSet<Order>>
// Nested group by multiple columns — leaves are RecordSets, not plain arrays
$byStatusByYear = $orders->recordsGroupedByKeys('status', 'year');
// ['pending' => [2024 => RecordSet<Order>, 2025 => RecordSet<Order>], …]
// Convert to raw arrays (column → scalar) — useful for serialisation
$rows = $orders->toArraySet(); // list<array<string, scalar|null>>
// Stamp a shared field on every record before saving (e.g. updated_at, actor_id)
$set = new RecordSet([$line1, $line2, $line3]);
$set->bulkSet(['updated_by' => $userId]);
// Batch upsert — deadlock-safe 3-step strategy for all dirty records, one atomic transaction
$result = $set->upsertAll(); // ?SaveResult — null when nothing to save
$result->inserted; // rows newly written
$result->updated; // rows overwritten
$result->total(); // inserted + updated
$result->insertedIds; // list<int|string> — PKs of newly inserted auto-increment records
// Force-save (skip dirty filter) — useful in tests and for re-asserting state
$set->upsertAll(force: true);
// Bulk delete
$deleted = $set->deleteAll(); // DELETE FROM … WHERE id IN (…)
// 50k rows written 1000 at a time; each 1000-row chunk commits before the next begins
$set->upsertAll(chunkSize: 1000);
// one statement, no row locks — a PK-keyed coalescing outbox/queue write
(new RecordSet($rows))->upsertAll(strategy: UpsertStrategy::Lockless);
// append 3 ledger rows with client-minted UUIDv7 PKs — one INSERT, no upsert, no locks
$result = (new RecordSet([$e1, $e2, $e3]))->insertAll();
// seed recommended defaults only where no row exists; a prior row is left untouched
(new RecordSet($rows))->insertAll(onConflict: OnConflict::Ignore);
$row->save(onConflict: OnConflict::Ignore); // single-row sibling
// $rows are PK-less Records whose conflict-key column(s) are set
$result = (new RecordSet($rows))->upsertAllByUniqueKey('uniq_owner_code');
$order->save(readBack: true); // reload the whole row (fires afterLoad())
$order->save(readBack: ['total', 'tax']); // patch just these columns
$order->save(readBack: false); // never
$order->save(); // readBack: null (default) = auto
// One extra query per level
$orders = Order::find('`status` = ?', ['pending'])
->load('lines'); // SELECT … WHERE order_id IN (…)
foreach ($orders as $order) {
foreach ($order->lines as $line) {
echo $line->sku;
}
}
// Dot-notation chains
$orders->load('lines.product'); // loads lines, then products for those lines
// Skip records that already have the relation loaded
$orders->loadMissing('lines.product');
// The same API is on a single record
$order->load('lines', 'customer.billing');
// Parent side — Order has many Tags
#[Table(name: 'orders')]
class Order extends Record
{
// …
/** @var RecordSet<Tag>|null */
#[Relation(RelationType::MorphMany, class: Tag::class,
morphType: 'tagable_type', morphKey: 'tagable_id',
morphValue: 'order')]
public ?RecordSet $tags = null;
// MorphOne: same as MorphMany but returns a single record or null
#[Relation(RelationType::MorphOne, class: Tag::class,
morphType: 'tagable_type', morphKey: 'tagable_id',
morphValue: 'order')]
public ?Tag $primaryTag = null;
}
// Child side — Tag belongs to a polymorphic parent
#[Table(name: 'tags')]
class Tag extends Record
{
#[Column(ColumnType::VarChar, length: 50)]
public string $tagable_type = '';
#[Column(ColumnType::BigIntUnsigned)]
public int $tagable_id = 0;
#[Relation(RelationType::MorphTo,
morphType: 'tagable_type',
morphKey: 'tagable_id',
morphMap: ['order' => Order::class, 'product' => Product::class])]
public Order|Product|null $tagable = null;
}
// Load orders with all their tags — one extra query
$orders = Order::find('`status` = ?', ['pending'])->load('tags');
foreach ($orders as $order) {
foreach ($order->tags as $tag) {
echo $tag->name;
}
}
// Load tags with their polymorphic parent — one query per distinct type present
$tags = Tag::find()->load('tagable');
foreach ($tags as $tag) {
// $tag->tagable is an Order or Product depending on tagable_type
}
// Chains work too: orders → tags → tagable (round-trip)
$orders->load('tags.tagable');
// bind a specific session for the duration of the closure; restored afterward (even on throw)
Outbox::usingSession($engineSession, static fn () =>
(new RecordSet($rows))->upsertAll(strategy: UpsertStrategy::Lockless),
);
// or bind a full Connection (session + dialect)
Order::usingConnection($conn, static fn () => $order->save());
#[Column(ColumnType::IntUnsigned)]
#[Version]
public int $version = 1;
// two processes both loaded this row at version 4…
$a->qty = 10; $a->save(); // → version 5, OK
$b->qty = 20; $b->save(); // → OptimisticLockException (guard expected 4, row is now 5)
use Nandan108\Attrecord\Attribute\LockTier;
#[Table(name: 'orders')]
#[LockTier(1)]
class Order extends Record { ... }
#[Table(name: 'order_lines')]
#[LockTier(2)]
class OrderLine extends Record { ... }
use Nandan108\Attrecord\LockSet;
PurchaseOrder::transactional(function (Transaction $tx) use ($poId, $lineIds, $slotId): void {
$locks = LockSet::acquire(PurchaseOrder::connection(), [
PurchaseOrder::class => [$poId],
PurchaseOrderLine::class => $lineIds,
InventorySlot::class => [$slotId],
], $tx);
// $locks[PurchaseOrder::class] is RecordSet<PurchaseOrder>
// $locks[PurchaseOrderLine::class] is RecordSet<PurchaseOrderLine>
foreach ($locks[PurchaseOrderLine::class] as $line) {
// … process under lock
}
});
$conn = Record::connection();
$conn->session->withAdvisoryLock(
lockName: 'invflux.reconcile.shipment-42',
timeoutSeconds: 5, // 0 = fail immediately, -1 = wait indefinitely
callback: function () {
// ... serialise this critical section across all PHP workers
},
);
use Nandan108\Attrecord\Session\PdoDbSession;
use Nandan108\Attrecord\Dialect\{MysqlDialect, PgsqlDialect, SqliteDialect};
// MySQL / MariaDB
$pdo = new PDO('mysql:host=127.0.0.1;dbname=shop', 'user', 'pass');
$conn = new Connection(new PdoDbSession($pdo), new MysqlDialect());
// PostgreSQL — same adapter, paired with PgsqlDialect
$pdo = new PDO('pgsql:host=127.0.0.1;dbname=shop', 'user', 'pass');
$conn = new Connection(new PdoDbSession($pdo), new PgsqlDialect());
// SQLite — same adapter, paired with SqliteDialect (file-based or :memory:)
$pdo = new PDO('sqlite:/var/data/shop.sqlite');
$conn = new Connection(new PdoDbSession($pdo), new SqliteDialect());
new SqliteDialect(
journalMode: 'WAL', // PRAGMA journal_mode (default 'WAL'; pass null to leave the default)
busyTimeoutMs: 5000, // PRAGMA busy_timeout in ms (default 5000; null to skip)
foreignKeys: true, // PRAGMA foreign_keys=ON (default true — SQLite defaults it OFF)
);
use Nandan108\Attrecord\Session\MysqliDbSession;
$mysqli = new mysqli('127.0.0.1', 'user', 'pass', 'shop');
$conn = new Connection(new MysqliDbSession($mysqli), new MysqlDialect());
use Nandan108\Attrecord\Session\WpDbSession;
global $wpdb;
$conn = new Connection(new WpDbSession($wpdb), new MysqlDialect());
Record::setConnection($conn);
use Nandan108\Attrecord\Session\{RetryingDbSession, PdoDbSession};
use Nandan108\Attrecord\Dialect\PgsqlDialect;
$conn = new Connection(new RetryingDbSession(new PdoDbSession($pdo)), new PgsqlDialect());
public function __construct(
DbSession $inner, // the session to wrap
int $maxAttempts = 10, // total attempts, including the first
int $baseDelayUs = 5_000, // base backoff in µs, doubled each attempt
int $maxDelayUs = 100_000, // per-attempt backoff cap in µs
?\Closure $retryable = null, // (\Throwable): bool — overrides the default classification
)
use Nandan108\Attrecord\Test\CapturingDbSession;
use Nandan108\Attrecord\Connection;
use Nandan108\Attrecord\Dialect\MysqlDialect;
$session = new CapturingDbSession();
Record::setConnection(new Connection($session, new MysqlDialect()));
$order = new Order();
$order->status = 'pending';
$order->save();
assertStringContainsString('INSERT INTO `orders`', $session->lastSql());
assertSame(['pending'], $session->lastParams());
// Control the returned PK
$session->setNextInsertId(100);
$order2 = new Order();
$order2->status = 'draft';
$order2->save();
assertSame(100, $order2->id);
// Full call log
$session->allCalls(); // list<array{sql: string, params: list<scalar|null>}>
$session->reset(); // clear log
#[Table(name: 'orders', primaryKey: 'order_id')]
final class Order extends Record
{
#[Column(ColumnType::BigIntUnsigned, name: 'order_id', autoIncrement: true)]
public ?int $orderId = null;
#[Column(ColumnType::BigIntUnsigned, name: 'customer_id')]
public int $customerId = 0;
// No `name:` override — column name equals property name
#[Column(ColumnType::VarChar, length: 20)]
public string $status = 'pending';
}
#[Column(
type: ColumnType::VarChar,
name: 'col_name', // SQL column name override (defaults to PHP property name)
nullable: true, // allows NULL; PHP property becomes ?string
autoIncrement: true, // skipped in INSERT/UPDATE; PK assigned after INSERT
trimOnSave: true, // trim whitespace on save; also suppresses dirty-detection for whitespace-only changes
length: 255, // for VarChar/Char/Binary/VarBinary; also enforced at DDL generation time
precision: 10, // Decimal: total digits (
use Nandan108\Attrecord\BinaryParam;
use Nandan108\Attrecord\WhereClause;
Subject::find(WhereClause::where('uuid', new BinaryParam($rawBytes)));
// Standard relation
#[Relation(
type: RelationType::OneToMany,
class: OrderLine::class, // target Record subclass
foreignKey: 'order_id', // FK column name
localKey: 'id', // optional; defaults to this table's PK
)]
// Polymorphic parent
#[Relation(
type: RelationType::MorphMany,
class: Tag::class,
morphType: 'tagable_type', // type-discriminator column on the related table
morphKey: 'tagable_id', // FK column on the related table
morphValue: 'order', // value stored in morphType for this class (string or int)
)]
// Polymorphic child
#[Relation(
type: RelationType::MorphTo,
morphType: 'tagable_type', // local type-discriminator column
morphKey: 'tagable_id', // local FK column
morphMap: ['order' => Order::class, 'product' => Product::class],
)]
// Many-to-many through a pivot table (junction of two FK columns)
#[Relation(
type: RelationType::ManyToMany,
class: Tag::class,
pivotTable: 'post_tag', // junction table
pivotLocalKey: 'post_id', // pivot column → this record's PK
pivotForeignKey: 'tag_id', // pivot column → the target's PK
)]
// Has-many-through an intermediate Record (reach the far records, skip the middle)
#[Relation(
type: RelationType::HasManyThrough,
class: Comment::class, // far records
through: Post::class, // intermediate Record
foreignKey: 'user_id', // intermediate column → this record's PK
secondKey: 'post_id', // far column → the intermediate's PK
)]
Loading please wait ...
Before you can download the PHP files, the dependencies should be resolved. This can take some minutes. Please be patient.