The annotated entities' extension is capable of indexing any domain entity in your project. To indicate that the class
must be treated as a domain entity make sure to add the #[Entity] attribute.
use Cycle\Annotated\Annotation\Entity;
#[Entity]
class User
{
// ...
}
Read more about Prerequisites and Setup for configuring the annotation compiler pipeline.
Usually, the single attribute #[Entity] is enough to describe your model. In this case, Cycle ORM will automatically
assign the generated table name and role based on the class name. In the case of User the role will be user,
database null (default) and table users.
You can tweak all of these values by setting entity options:
use Cycle\Annotated\Annotation\Entity;
#[Entity(role: 'user', database: 'database', table: 'user_table')]
class User
{
// ...
}
You must manually set
roleandtablefor your classes if you use models that share the same name.
Some options can be used to overwrite default entity behaviour, for example to assign a custom entity repository:
use Cycle\Annotated\Annotation\Entity;
#[Entity(repository: Repository\UserRepository::class)]
class User
{
// ...
}
Cycle ORM can locate repository class names automatically, using current entity namespace as the base path.
| Parameter | Type | Default | Description |
|---|---|---|---|
| role | ?string | null | Entity role. Defaults to the lowercase class name without a namespace |
| mapper | ?class-string | null | Mapper class name. Defaults to Cycle\ORM\Mapper\Mapper |
| repository | ?class-string | null | Repository class to represent read operations for an entity. Defaults to Cycle\ORM\Select\Repository |
| table | ?string | null | Entity source table. Defaults to plural form of entity role |
| readonlySchema | bool | false | Set to true to disable schema synchronization for the assigned table |
| database | ?string | null | Database name. Defaults to default database |
| source | ?class-string | null | Entity source class (internal). Defaults to Cycle\ORM\Select\Source |
| typecast | string|string[]|null | null | Typecast handler class name or array of handler class names. Defaults to Cycle\ORM\Parser\Typecast. Read more about typecasting |
| scope | ?class-string | null | Class name of constraint to be applied to every entity query. Read more about scopes |
| columns | Column[] | [] | Additional entity columns for unmapped properties. See Class-Level Columns |
| foreignKeys | ForeignKey[] | [] | Foreign key definitions without relations. See Foreign Keys |
A typical entity description with multiple options:
use Cycle\Annotated\Annotation\Entity;
#[Entity(
table: 'users',
repository: \App\Repository\UserRepository::class,
scope: \App\Scope\SortByID::class,
typecast: [\App\Typecast\JsonTypecast::class, \Cycle\ORM\Parser\Typecast::class]
)]
class User
{
// ...
}
No entity can operate without some properties mapped to table columns. To map your property to the column add the
attribute #[Column] to it. It's mandatory to specify the column type. You must always specify only one auto
incremental column (type: 'primary'), but one or more non-incremental primary columns (primary: true).
Read more about composite keys.
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Column;
#[Entity]
class User
{
#[Column(type: 'primary')]
private int $id;
}
#[Entity]
class Pivot
{
#[Column(type: 'int', primary: true)]
private int $postId;
#[Column(type: 'int', primary: true)]
private int $tagId;
}
Read how to use non-incremental primary keys in the Advanced section:
You can use multiple attributes at the same time with shorter syntax:
use Cycle\Annotated\Annotation as Cycle;
#[Cycle\Entity]
class User
{
#[Cycle\Column(type: 'primary')]
private int $id;
}
By default, the entity property will be mapped to the column with the same name as the property. You can change it as follows:
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Column;
#[Entity]
class User
{
#[Column(type: 'primary')]
private int $id;
#[Column(type: 'string', name: 'username')]
private string $login;
}
Some column types support additional arguments, such as length, values, etc.
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Column;
#[Entity]
class User
{
#[Column(type: 'primary')]
private int $id;
#[Column(type: 'string(32)')]
private string $login;
#[Column(type: 'enum(active,disabled)')]
private string $status;
#[Column(type: 'decimal(5,5)')]
private $balance;
}
Use the default option to specify the default value of the column:
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Column;
#[Entity]
class User
{
#[Column(type: 'primary')]
private int $id;
#[Column(type: 'enum(active,disabled)', default: 'active')]
private string $status;
}
While adding new columns to entities associated with non-empty tables you are required to either specify a default value or mark the column as nullable:
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Column;
#[Entity]
class User
{
#[Column(type: 'primary')]
protected int $id;
#[Column(type: 'string(64)', nullable: true)]
protected ?string $password = null;
}
| Parameter | Type | Default | Description |
|---|---|---|---|
| type | string | - | Required. Column type with optional arguments. See Column Types Reference |
| name | ?string | null | Column name in database. Defaults to the property name |
| property | ?string | null | Entity property name. Used for virtual columns that don't map to entity properties |
| primary | bool | false | Explicitly mark column as part of primary key |
| nullable | bool | false | Allow NULL values in column |
| default | mixed | null | Default column value |
| castDefault | bool | false | Apply typecast to default value |
| typecast | callable|string|null | null | Typecast rule. Can be callable or string for default handler: "int", "float", "bool", "datetime". Read more about typecasting |
| readonlySchema | bool | false | Set to true to disable schema synchronization for this column |
| ...$attributes | mixed | - | Database-specific attributes using named parameters. Example: #[Column('smallInt', unsigned: true, zerofill: true)] |
| Type | Parameters | Description |
|---|---|---|
| primary | - | Auto-incrementing integer primary key (typically 32-bit). Only one primary column allowed per entity |
| bigPrimary | - | Auto-incrementing big integer primary key (typically 64-bit) |
| boolean | - | Boolean type (stored as tinyint 0/1 in most databases) |
| integer | - | Standard integer (typically 32-bit) |
| tinyInteger | - | Small integer (typically 8-bit). Check DBMS for size limits |
| smallInteger | - | Small integer (typically 16-bit). Check DBMS for size limits |
| bigInteger | - | Large integer (typically 64-bit) |
| string | [length:255] | Variable-length string. Ideal for indexed fields like emails and usernames |
| text | - | Large text field. Check DBMS for size limitations |
| tinyText | - | Small text field (MySQL specific, same as text in other databases) |
| longText | - | Very large text field (MySQL specific, same as text in other databases) |
| double | - | Double precision floating-point number |
| float | - | Single precision floating-point number |
| decimal | precision, [scale:0] | Fixed-precision decimal number. Example: decimal(10,2) for currency |
| datetime | - | Date and time (automatically uses UTC timezone) |
| date | - | Date only (automatically uses UTC timezone) |
| time | - | Time only |
| timestamp | - | Unix timestamp (stored as integer). Automatically converts to UTC. Prefer datetime for general date/time storage |
| binary | - | Binary data. Check DBMS for size limitations |
| tinyBinary | - | Small binary field (MySQL specific) |
| longBinary | - | Large binary field (MySQL specific) |
| json | - | JSON data. Native support in PostgreSQL and MySQL 5.7+, stored as text in other databases |
| uuid | - | UUID/GUID field |
| ulid | - | ULID (Universally Unique Lexicographically Sortable Identifier) |
| snowflake | - | Snowflake ID (Twitter's distributed unique ID) |
The ORM supports enum columns with explicit value lists. You can define enums in three ways:
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Column;
#[Entity]
class User
{
#[Column(type: 'primary')]
private int $id;
#[Column(type: 'enum(active,disabled,banned)', default: 'active')]
private string $status;
}
You can use PHP's native backed enums with the values parameter:
enum UserStatus: string
{
case Active = 'active';
case Disabled = 'disabled';
case Banned = 'banned';
}
#[Entity]
class User
{
#[Column(type: 'primary')]
private int $id;
#[Column(type: 'enum', values: UserStatus::class, default: 'active')]
private string $status;
// Or pass a specific enum instance
#[Column(type: 'enum', values: UserStatus::Active)]
private string $defaultActive;
}
#[Entity]
class User
{
#[Column(type: 'enum', values: ['active', 'disabled', 'banned'])]
private string $status;
// Mix of strings and enum values
#[Column(type: 'enum', values: [UserStatus::Active, 'custom'])]
private string $mixedStatus;
}
Beyond the standard column types, you can use any database-specific types supported by your DBMS. Cycle ORM passes these types directly to the database driver, allowing you to leverage advanced features unique to PostgreSQL, MySQL, SQL Server, and other databases.
Simply specify the native type name in the type parameter:
use Cycle\Annotated\Annotation\Column;
#[Entity]
class PostgresEntity
{
// PostgreSQL JSONB with indexing support
#[Column(type: 'jsonb')]
private array $metadata;
// PostgreSQL timestamp with timezone
#[Column(type: 'timestamptz')]
private \DateTimeInterface $scheduledAt;
// PostgreSQL geometric types
#[Column(type: 'point')]
private string $location;
// PostgreSQL network address types
#[Column(type: 'inet')]
private string $ipAddress;
}
#[Entity]
class SQLServerEntity
{
// SQL Server high-precision datetime
#[Column(type: 'datetime2')]
private \DateTimeInterface $timestamp;
}
Use named parameters to pass vendor-specific column attributes:
// MySQL: unsigned integers and zerofill
#[Column(type: 'integer', unsigned: true, zerofill: true)]
private int $count;
// MySQL: string with specific character set and collation
#[Column(type: 'string', length: 100, charset: 'utf8mb4', collation: 'utf8mb4_unicode_ci')]
private string $name;
// PostgreSQL: array column
#[Column(type: 'text', array: true)]
private array $tags;
// Any database: decimal with precision and scale
#[Column(type: 'decimal', precision: 10, scale: 2)]
private string $price;
Portability Tip: When building applications that might run on different databases, prefer standard column types. Use database-specific types only when you need features unavailable in the standard set.
The #[GeneratedValue] attribute marks entity fields whose values are automatically generated either by the database or
by the ORM during persistence operations. This is essential for fields like auto-increment IDs, timestamps, UUIDs, and
other automatically managed values.
Important: Without #[GeneratedValue], Cycle ORM treats null field values as actual NULL values to persist, which
can cause constraint violations on NOT NULL columns. This attribute tells the ORM to skip sending these fields to the
database when they're unset.
The attribute supports three generation modes that can be combined:
onInsert: true)The database generates the value when the row is inserted. The ORM retrieves the generated value after insertion.
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Column;
use Cycle\Annotated\Annotation\GeneratedValue;
#[Entity]
class User
{
// Auto-increment primary key
#[Column(type: 'primary')]
#[GeneratedValue(onInsert: true)]
private int $id;
// Database default timestamp (PostgreSQL: DEFAULT NOW())
#[Column(type: 'datetime', default: 'CURRENT_TIMESTAMP')]
#[GeneratedValue(onInsert: true)]
private \DateTimeInterface $registeredAt;
}
Use cases:
primary, bigPrimary)beforeInsert: true)The ORM generates the value in PHP before sending the INSERT query to the database.
#[Entity]
class Document
{
#[Column(type: 'uuid', primary: true)]
#[GeneratedValue(beforeInsert: true)]
private string $id;
#[Column(type: 'datetime')]
#[GeneratedValue(beforeInsert: true)]
private \DateTimeInterface $createdAt;
}
Use cases:
Read more about UUID generation in Entity Behaviors: UUID.
beforeUpdate: true)The ORM regenerates the value in PHP before each UPDATE query.
#[Entity]
class Article
{
#[Column(type: 'primary')]
#[GeneratedValue(onInsert: true)]
private int $id;
#[Column(type: 'datetime')]
#[GeneratedValue(beforeInsert: true)]
private \DateTimeInterface $createdAt;
// Automatically updated on every save
#[Column(type: 'datetime')]
#[GeneratedValue(beforeInsert: true, beforeUpdate: true)]
private \DateTimeInterface $updatedAt;
}
Use cases:
updatedAt, modifiedAt)You can combine beforeInsert and beforeUpdate for fields that need generation on both operations:
#[Entity]
class Post
{
#[Column(type: 'datetime')]
#[GeneratedValue(beforeInsert: true, beforeUpdate: true)]
private \DateTimeInterface $lastModified; // Set on create AND update
#[Column(type: 'datetime')]
#[GeneratedValue(beforeInsert: true)]
private \DateTimeInterface $createdAt; // Set only on create
}
| Parameter | Type | Default | Description |
|---|---|---|---|
| beforeInsert | bool | false | Generate value in PHP before INSERT. The ORM calls value generators before sending the query to database |
| onInsert | bool | false | Value is generated by database on INSERT (auto-increment, DEFAULT expressions). The ORM retrieves the value after insertion |
| beforeUpdate | bool | false | Regenerate value in PHP before UPDATE. The ORM calls value generators before sending the query. Ignored during INSERT |
Integration with Entity Behaviors: The
#[GeneratedValue]attribute works seamlessly with Cycle's Entity Behaviors system. Behaviors likeCreatedAt,UpdatedAt, andUuidautomatically set up proper generation logic. Read more in Entity Behaviors.
See also: For implementing custom value generation logic, explore the Advanced: Custom Generators section.
In some cases you might want to specify additional table columns and indexes without linking them to entity properties. Use class-level attributes to define table schema extensions.
Define columns that don't map to entity properties:
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Column;
use Cycle\Annotated\Annotation\Table\Index;
#[Entity]
#[Column(name: 'created_at', type: 'datetime')]
#[Column(name: 'deleted_at', type: 'datetime', nullable: true)]
#[Index(columns: ['username'], unique: true)]
#[Index(columns: ['name', 'id DESC'])]
class User
{
#[Column(type: 'primary')]
protected int $id;
#[Column(type: 'string(32)')]
protected string $username;
#[Column(type: 'string')]
protected string $name;
}
The column definition is identical to property-level
#[Column]attributes. Use thepropertyparameter to map class-level columns to entity properties.
Create indexes using the #[Index] attribute at class level:
use Cycle\Annotated\Annotation\Table\Index;
// Simple index
#[Index(columns: ['email'])]
// Unique index
#[Index(columns: ['username'], unique: true)]
// Named index
#[Index(columns: ['user_id', 'created_at'], name: 'user_created_idx')]
// Composite index with sort order
#[Index(columns: ['name ASC', 'id DESC'])]
| Parameter | Type | Default | Description |
|---|---|---|---|
| columns | string[] | - | Required. Array of column names |
| unique | bool | false | Create unique index |
| name | ?string | null | Index name. Auto-generated if not specified |
For composite primary keys, use the #[Table\PrimaryKey] attribute:
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Column;
use Cycle\Annotated\Annotation\Table\PrimaryKey;
#[Entity]
#[PrimaryKey(columns: ['user_id', 'post_id'])]
class UserPost
{
#[Column(type: 'integer')]
private int $user_id;
#[Column(type: 'integer')]
private int $post_id;
}
Read more about composite primary keys.
The Annotated Entities extension supports merging table definitions from linked Mapper, Source, Repository, and Scope classes. This approach is useful for implementing reusable domain functionality like timestamps or soft deletes.
use Cycle\Annotated\Annotation\Entity;
#[Entity(repository: Repository\UserRepository::class)]
class User
{
// ...
}
Define schema extensions in your repository:
use Cycle\Annotated\Annotation\Column;
use Cycle\Annotated\Annotation\Table\Index;
use Cycle\ORM\Select\Repository;
#[Column(name: 'created_at', type: 'datetime')]
#[Column(name: 'updated_at', type: 'datetime')]
#[Index(columns: ['created_at'])]
class UserRepository extends Repository
{
// Custom repository methods
}
This pattern allows you to:
Read more about custom repositories and entity behaviors.
In some cases, it is necessary to define a foreign key without relation definitions. This can be achieved using the
#[ForeignKey] attribute.
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Column;
use Cycle\Annotated\Annotation\ForeignKey;
#[Entity]
class User
{
#[Column(type: 'primary')]
public int $id;
}
#[Entity]
class Post
{
#[Column(type: 'primary')]
public int $id;
#[Column(type: 'integer')]
#[ForeignKey(target: User::class, action: 'CASCADE')]
private int $userId;
}
Alternatively, specify all foreign key details at the class level:
#[Entity]
#[ForeignKey(
target: User::class,
innerKey: 'user_id', // Column in this table
outerKey: 'id', // Column in target table
action: 'CASCADE'
)]
class Post
{
#[Column(type: 'primary')]
public int $id;
#[Column(type: 'integer')]
private int $user_id;
}
| Parameter | Type | Default | Description |
|---|---|---|---|
| target | string | - | Required. Target entity role or class name |
| innerKey | string|array | null | Column(s) in source entity. Required for class-level FK, inferred from property for property-level |
| outerKey | string|array | null | Column(s) in target entity. Defaults to primary key of target |
| action | string | 'CASCADE' | Foreign key action for both DELETE and UPDATE: 'CASCADE', 'NO ACTION', 'SET NULL' |
| indexCreate | bool | true | Automatically create index on innerKey |
Note:
MySQL and SQL Server may automatically create indexes for foreign keys. TheindexCreateoption ensures cross-database compatibility.
Read more about foreign keys in relations: BelongsTo Foreign Keys