cycle/annotated
Define Cycle ORM entities and schema using PHP 8 attributes. Annotate columns, primary keys, enums, decimals, and relationships like HasOne, HasMany, and BelongsTo, then let Cycle build the mapping from your code.
Installation
composer require cycle/annotated
Ensure cycle/orm is also installed (required dependency).
Basic Entity Definition
Create a PHP class with #[Entity] and #[Column] attributes:
use Cycle\Annotated\Annotation\{Entity, Column};
#[Entity]
class User {
#[Column(type: 'primary')]
public int $id;
#[Column(type: 'string(255)')]
public string $name;
}
Register the Schema Compiler
In your Laravel service provider (e.g., AppServiceProvider):
use Cycle\Annotated\Processor;
use Cycle\ORM\Factory;
public function boot(): void {
$factory = new Factory();
$factory->getPool()->registerCompiler(new Processor());
}
First Use Case Define a repository and fetch entities:
use Cycle\ORM\Select;
$users = $factory->getRepository(User::class)
->select(Select::all())
->fetchAll();
src/Annotation/ in the package source for attribute definitions.// Repository pattern (recommended)
$user = $factory->getRepository(User::class)->create([
'name' => 'John Doe',
]);
enum UserStatus {
case Active;
case Inactive;
}
#[Column(type: 'enum', values: [UserStatus::Active, UserStatus::Inactive])]
public UserStatus $status;
#[HasOne]/#[BelongsTo] with fetchMode: 'lazy' to defer loading:
#[HasOne(target: Address::class, fetchMode: 'lazy')]
public ?Address $address;
interface Imageable {
public function getImageableId(): int;
}
#[BelongsToMorphed(target: Imageable::class)]
public Imageable $imageable;
#[Embeddable]
class ContactInfo {
#[Column(type: 'string(100)')]
public string $email;
#[Column(type: 'string(20)')]
public string $phone;
}
#[Entity]
class User {
#[Embedded(target: ContactInfo::class)]
public ContactInfo $contact;
}
#[Column(type: 'timestamp', generated: 'insert')]
public ?string $createdAt;
#[Index]:
#[Column(type: 'string(255)')]
#[Index]
public string $email;
Cycle\ORM\Select for bulk queries:
$factory->getRepository(User::class)
->select(Select::all())
->where('status', '=', 'active')
->update(['balance' => fn($balance) => $balance + 100]);
$this->app->singleton(Factory::class, fn($app) => new Factory());
// app/Facades/CycleORM.php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
class CycleORM extends Facade {
protected static function getFacadeAccessor() {
return \Cycle\ORM\Factory::class;
}
}
Usage:
$users = CycleORM::getRepository(User::class)->fetchAll();
cycle/orm:
vendor/bin/cycle-make:migration User
AttributeReader is the default reader (set in cycle/orm config).#[Attribute\Target("CLASS")] or #[Attribute\Target("PROPERTY")] explicitly if needed.#[HasMany]/#[BelongsTo] loops cause infinite recursion.
fetchMode: 'lazy' or break cycles with #[RefersTo]:
#[RefersTo(target: Comment::class)]
public ?Comment $lastComment;
#[DiscriminatorColumn] is defined on the base class.#[InheritanceSingleTable].outerKey column (e.g., fooId).#[InheritanceJoinedTable(outerKey: 'fooId')].innerKey/outerKey.
#[ForeignKey] with innerKey/outerKey:
#[ForeignKey(
target: User::class,
innerKey: 'user_id',
outerKey: 'id',
action: 'CASCADE'
)]
public int $userId;
$factory->getSchemaCompiler()->compile();
// Throws exceptions for invalid configurations.
config/cycle.php:
'debug' => env('APP_DEBUG', false),
'logger' => [
'class' => \Monolog\Logger::class,
'level' => \Monolog\Logger::DEBUG,
],
$metadata = $factory->getSchemaCompiler()->getMetadata(User::class);
dd($metadata->getTable(), $metadata->getColumns());
cycle/orm's TypeSystem:
use Cycle\ORM\Type\TypeSystem;
$typeSystem = new TypeSystem();
$typeSystem->register('ulid', new ULIDType());
'naming' => [
'strategy' => \Cycle\ORM\Naming\SnakeCaseStrategy::class,
],
#[Embeddable(typecast: [
'email' => 'string',
'age' => 'int',
])]
class ContactInfo { ... }
Cycle\Annotated\Annotation\Annotation:
#[Attribute(Attribute::TARGET_PROPERTY)]
class SoftDelete extends Annotation {
public function __construct(
public string $column = 'deleted_at',
public string $default = '0000-00-00 00:00:00'
) {}
}
beforeInsert):
use Cycle\ORM\Event\EventInterface;
$factory->getEventManager()->addListener(
EventInterface::BEFORE_INSERT,
fn($event) => $event->getEntity()->setCreatedAt(now())
);
Cycle\Annotated\Processor for custom logic:
class CustomProcessor extends Processor {
How can I help you explore Laravel packages today?