cycle/annotated
Define Cycle ORM entity schemas using PHP attributes instead of annotations or config. Map columns, primary keys, and HasOne, HasMany, and BelongsTo relations directly in your entity classes.
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?