cycle/orm
Cycle ORM is a PHP DataMapper and modeling engine for safe use in classic and long-running apps (e.g., RoadRunner). Supports POPOs, flexible schemas, rich relations, eager/lazy loading, powerful queries, and MySQL/Postgres/SQLite/SQLServer.
## Getting Started
### **First Steps**
1. **Installation**
Add the package via Composer:
```bash
composer require cycle/orm
Register the Cycle ORM service provider in config/app.php:
'providers' => [
Cycle\ORM\CycleServiceProvider::class,
],
Configuration
Define your database connection in config/cycle.php:
'connections' => [
'default' => [
'orm' => 'Cycle\\ORM\\Mapper\\MapperORM',
'db' => [
'driver' => 'pdo_mysql',
'host' => env('DB_HOST'),
'port' => env('DB_PORT'),
'database' => env('DB_DATABASE'),
'username' => env('DB_USERNAME'),
'password' => env('DB_PASSWORD'),
],
],
],
First Model
Define an entity (e.g., User) with annotations:
use Cycle\Annotated;
#[ORM\Entity]
class User
{
#[ORM\Column(type: 'primary')]
public int $id;
#[ORM\Column(type: 'string')]
public string $name;
#[ORM\Column(type: 'string')]
public string $email;
#[ORM\Column(type: 'json')] // Now supports scalar JSON values
public ?array $metadata = null;
}
First Query Inject the repository into a service or controller:
use Cycle\ORM\Select;
use Cycle\ORM\Repository;
class UserService
{
public function __construct(
private Repository $repository,
) {}
public function findByEmail(string $email): ?User
{
return $this->repository
->get(User::class)
->where('email', '=', $email)
->fetchOne();
}
// Stream entities lazily (new in v2.16.0)
public function streamUsers(): \Generator
{
yield from $this->repository
->get(User::class)
->cursor(); // New cursor() method
}
}
CRUD Operations Use repositories for all CRUD:
// Create
$user = new User();
$user->name = 'John';
$user->email = 'john@example.com';
$user->metadata = ['role' => 'admin']; // JSON column
$this->repository->persist($user);
// Read
$user = $this->repository->get(User::class)->find($user->id);
Query Building Chain methods for complex queries:
$users = $this->repository
->get(User::class)
->where('active', '=', true)
->orderBy('name')
->limit(10)
->fetchAll();
Streaming Entities (New in v2.16.0)
Use cursor() for lazy-loading large datasets:
foreach ($this->repository->get(User::class)->cursor() as $user) {
// Process one user at a time (memory-efficient)
}
Relationships Define relationships via annotations:
#[ORM\ManyToMany(target: Post::class)]
public array $posts;
Fetch related data:
$user = $this->repository
->get(User::class)
->with('posts')
->find($userId);
Transactions
Use Transaction for atomic operations:
$transaction = $this->repository->transaction();
try {
$transaction->persist($user);
$transaction->persist($post);
$transaction->commit();
} catch (\Throwable $e) {
$transaction->rollBack();
throw $e;
}
JSON Column Support (Improved in v2.16.0) Store scalar JSON values directly:
$user->metadata = ['preferences' => ['theme' => 'dark']];
$this->repository->persist($user);
// Retrieve as array
$metadata = $user->metadata; // Returns array, not string
Laravel Integration
Bind the repository to Laravel’s container in CycleServiceProvider:
$this->app->bind(Repository::class, function ($app) {
return $app->make('cycle.orm')->getRepository();
});
Use dependency injection in controllers:
public function __construct(private Repository $repository) {}
Migrations Use Cycle’s schema builder for migrations:
use Cycle\ORM\Blueprint;
$schema = $this->repository->getSchema();
$schema->createTable('users', function (Blueprint $table) {
$table->integer('id')->primary();
$table->string('name');
$table->string('email')->unique();
$table->json('metadata')->nullable(); // JSON column
});
Testing
Use Cycle\ORM\Test\ORMTestCase for isolated tests:
use Cycle\ORM\Test\ORMTestCase;
class UserTest extends ORMTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->repository->getSchema()->createTable('users', ...);
}
public function testStreaming()
{
$this->repository->get(User::class)->persistMany(array_fill(0, 1000, new User()));
$count = 0;
foreach ($this->repository->get(User::class)->cursor() as $user) {
$count++;
}
$this->assertEquals(1000, $count);
}
}
Lazy Loading
Avoid N+1 queries by using with() or fetchMode(FetchMode::JOIN):
$user = $this->repository
->get(User::class)
->with('posts')
->find($userId);
Transaction Isolation Ensure transactions are properly committed/rolled back:
// ❌ Forgetting to commit
$transaction->persist($user); // No commit!
// ✅ Correct
try {
$transaction->persist($user);
$transaction->commit();
} catch (\Throwable $e) {
$transaction->rollBack();
}
Schema Mismatches
Always run schema:update after changing entity annotations:
php artisan cycle:schema:update
Case Sensitivity Column names in annotations must match the database schema exactly (including case).
Circular References Avoid infinite loops in relationships:
// ❌ Circular (User <-> Post <-> User)
#[ORM\ManyToMany(target: User::class)]
public array $authors;
JSON Column Serialization (v2.16.0) Ensure JSON columns are properly typed:
// ✅ Correct (scalar JSON)
#[ORM\Column(type: 'json')]
public ?array $metadata = null;
// ❌ Avoid (string JSON)
#[ORM\Column(type: 'string')]
public ?string $metadataJson = null; // Not recommended for structured data
Query Logging
Enable SQL logging in config/cycle.php:
'debug' => [
'query' => true,
],
Entity Dumping
Use dump() to inspect entities:
$user = $this->repository->get(User::class)->find(1);
dump($user->toArray());
Schema Inspection Dump the schema to verify tables/columns:
$schema = $this->repository->getSchema();
dump($schema->getTable('users')->getColumns());
Streaming Debugging
Verify lazy-loading with cursor():
$cursor = $this->repository->get(User::class)->cursor();
foreach ($cursor as $user) {
if ($cursor->isFinished()) break;
// Process user
}
Custom JSON Types
Extend Cycle\ORM\Column\Type for custom JSON handling:
class CustomJsonType extends Type
{
public function getSQLType(): string { return 'json'; }
public function encode($value): string { return json_encode($value, JSON_PRETTY_PRINT); }
public function decode(string $value): mixed { return json_decode($value, true); }
}
Custom Repositories Create a base repository class with streaming
How can I help you explore Laravel packages today?