# Cycle ORM Cycle ORM is a PHP DataMapper ORM and Data Modelling engine designed to safely work in classic and daemonized PHP applications (like RoadRunner). It provides flexible configuration options to model datasets, a powerful query builder, and supports dynamic mapping schemas. The engine works with plain PHP objects, supports annotation declarations, and proxies via extensions. The ORM implements the Data Mapper pattern with a Unit of Work for tracking entity changes and managing persistence. It supports various relation types including has-one, has-many, many-to-many, and polymorphic relations. Cycle ORM is designed for long-running applications with an immutable service core and disposable Unit of Work, making it suitable for modern PHP architectures like RoadRunner and Swoole. ## Core Components ### ORM Instance Creation The ORM class is the central entry point that provides access to repositories, mappers, and the entity heap. It requires a Factory and Schema to be initialized. ```php 'default', 'databases' => [ 'default' => ['connection' => 'sqlite'] ], 'connections' => [ 'sqlite' => [ 'driver' => \Cycle\Database\Driver\SQLite\SQLiteDriver::class, 'connection' => 'sqlite:database.db', ] ] ])); // Define entity schema $schema = new Schema([ 'user' => [ SchemaInterface::ENTITY => User::class, SchemaInterface::MAPPER => Mapper::class, SchemaInterface::DATABASE => 'default', SchemaInterface::TABLE => 'users', SchemaInterface::PRIMARY_KEY => 'id', SchemaInterface::COLUMNS => ['id', 'email', 'name', 'balance'], SchemaInterface::RELATIONS => [], ], ]); // Create ORM instance $orm = new ORM( factory: new Factory($dbal), schema: $schema, ); // Access services $heap = $orm->getHeap(); $factory = $orm->getFactory(); $schema = $orm->getSchema(); ``` ### Entity Manager - Persisting Entities The EntityManager handles entity persistence through a Unit of Work pattern. It supports both immediate state persistence and deferred persistence. ```php name = 'John Doe'; $user->email = 'john@example.com'; $user->balance = 100.00; // Persist with deferred state sync (changes after persist() are included) $em->persist($user); $user->balance = 150.00; // This change WILL be saved // Or persist with immediate state capture (changes after persistState() are ignored) $em->persistState($user); $user->balance = 200.00; // This change will NOT be saved // Execute all pending operations $state = $em->run(); if ($state->isSuccess()) { echo "Entity saved with ID: " . $user->id; } else { echo "Error: " . $state->getLastError()->getMessage(); } // Delete an entity $em->delete($user); $em->run(); // Run without throwing exceptions $state = $em->run(throwException: false); if (!$state->isSuccess()) { // Handle error gracefully $error = $state->getLastError(); } // Clean the entity manager (reset pending operations) $em->clean(); // Clean with heap reset $em->clean(cleanHeap: true); ``` ### Repository - Finding Entities Repositories provide a clean interface for querying entities. Each entity role has an associated repository. ```php getRepository(User::class); // Find by primary key $user = $userRepository->findByPK(1); // Find one by conditions $user = $userRepository->findOne(['email' => 'john@example.com']); // Find all matching conditions $users = $userRepository->findAll(['status' => 'active']); // Get entity directly from ORM (uses heap cache) $user = $orm->get(User::class, ['id' => 1]); // Force load from database (bypass heap) $user = $orm->get(User::class, ['id' => 1], load: true); // Check if entity exists in heap without loading $user = $orm->get(User::class, ['id' => 1], load: false); ``` ### Select Query Builder The Select class provides a fluent query builder for complex queries with relation loading. ```php where('status', 'active') ->where('balance', '>', 100) ->orderBy('name', 'ASC') ->limit(10) ->offset(0) ->fetchAll(); // Find by primary key $user = (new Select($orm, User::class)) ->wherePK(1) ->fetchOne(); // Multiple primary keys $users = (new Select($orm, User::class)) ->wherePK(1, 2, 3) ->fetchAll(); // Complex conditions with callbacks $users = (new Select($orm, User::class)) ->where(function ($q) { $q->where('status', 'active') ->orWhere('role', 'admin'); }) ->fetchAll(); // Aggregations $count = (new Select($orm, User::class))->count(); $avgBalance = (new Select($orm, User::class))->avg('balance'); $maxBalance = (new Select($orm, User::class))->max('balance'); $minBalance = (new Select($orm, User::class))->min('balance'); $totalBalance = (new Select($orm, User::class))->sum('balance'); // Get raw SQL $sql = (new Select($orm, User::class)) ->where('status', 'active') ->sqlStatement(); // Fetch as array data (not entities) $data = (new Select($orm, User::class)) ->fetchData(typecast: true); ``` ### Eager Loading Relations Load related entities efficiently using the `load()` method to prevent N+1 query problems. ```php load('profile') ->fetchAll(); // Load multiple relations $users = (new Select($orm, User::class)) ->load(['profile', 'comments', 'orders']) ->fetchAll(); // Load nested relations (user -> posts -> comments) $users = (new Select($orm, User::class)) ->load('posts.comments') ->fetchAll(); // Load with conditions using placeholder {@} for table alias $users = (new Select($orm, User::class)) ->load('comments', [ 'where' => ['{@}.approved' => true], 'orderBy' => ['{@}.created_at' => 'DESC'], ]) ->fetchAll(); // Control loading method $users = (new Select($orm, User::class)) ->load('orders', [ 'method' => Select::SINGLE_QUERY, // Use LEFT JOIN 'load' => function ($q) { $q->where('paid', true) ->orderBy('created_at', 'DESC'); } ]) ->fetchAll(); // Load with OUTER_QUERY (separate query, default for has-many) $users = (new Select($orm, User::class)) ->load('orders', ['method' => Select::OUTER_QUERY]) ->fetchAll(); // Many-to-many with pivot conditions $users = (new Select($orm, User::class)) ->load('tags', [ 'wherePivot' => ['{@}.approved' => true] ]) ->fetchAll(); ``` ### Join Relations for Filtering Use `with()` to join relations for filtering without loading the relation data. ```php distinct() ->with('comments') ->fetchAll(); // Filter by related entity field $users = (new Select($orm, User::class)) ->with('comments') ->where('comments.approved', true) ->fetchAll(); // Nested relation filtering $users = (new Select($orm, User::class)) ->with('posts.comments') ->where('posts_comments.approved', true) ->fetchAll(); // Custom join alias $users = (new Select($orm, User::class)) ->with('posts.comments', ['as' => 'comments']) ->where('comments.approved', true) ->fetchAll(); // Many-to-many pivot table filtering $users = (new Select($orm, User::class)) ->with('tags') ->where('tags_pivot.approved', true) ->fetchAll(); // Combine with() and load() - filter and load $users = (new Select($orm, User::class)) ->with('comments') ->where('comments.approved', true) ->load('comments', [ 'using' => 'comments' // Reuse the joined table ]) ->fetchAll(); ``` ### Schema Definition Define entity schemas programmatically with relations, typecasting, and other options. ```php [ SchemaInterface::ENTITY => User::class, SchemaInterface::MAPPER => Mapper::class, SchemaInterface::DATABASE => 'default', SchemaInterface::TABLE => 'users', SchemaInterface::PRIMARY_KEY => 'id', SchemaInterface::COLUMNS => [ 'id' => 'id', // property => column 'email' => 'email', 'name' => 'name', 'createdAt' => 'created_at', ], SchemaInterface::TYPECAST => [ 'id' => 'int', 'createdAt' => 'datetime', ], SchemaInterface::RELATIONS => [ 'profile' => [ Relation::TYPE => Relation::HAS_ONE, Relation::TARGET => 'profile', Relation::SCHEMA => [ Relation::CASCADE => true, Relation::INNER_KEY => 'id', Relation::OUTER_KEY => 'user_id', ], ], 'posts' => [ Relation::TYPE => Relation::HAS_MANY, Relation::TARGET => 'post', Relation::SCHEMA => [ Relation::CASCADE => true, Relation::INNER_KEY => 'id', Relation::OUTER_KEY => 'user_id', ], ], ], ], // Profile entity with belongs-to relation 'profile' => [ SchemaInterface::ENTITY => Profile::class, SchemaInterface::MAPPER => Mapper::class, SchemaInterface::DATABASE => 'default', SchemaInterface::TABLE => 'profiles', SchemaInterface::PRIMARY_KEY => 'id', SchemaInterface::COLUMNS => ['id', 'user_id', 'bio', 'avatar'], SchemaInterface::RELATIONS => [ 'user' => [ Relation::TYPE => Relation::BELONGS_TO, Relation::TARGET => 'user', Relation::SCHEMA => [ Relation::CASCADE => true, Relation::INNER_KEY => 'user_id', Relation::OUTER_KEY => 'id', ], ], ], ], // Many-to-many relation example 'post' => [ SchemaInterface::ENTITY => Post::class, SchemaInterface::MAPPER => Mapper::class, SchemaInterface::DATABASE => 'default', SchemaInterface::TABLE => 'posts', SchemaInterface::PRIMARY_KEY => 'id', SchemaInterface::COLUMNS => ['id', 'user_id', 'title', 'content'], SchemaInterface::RELATIONS => [ 'tags' => [ Relation::TYPE => Relation::MANY_TO_MANY, Relation::TARGET => 'tag', Relation::SCHEMA => [ Relation::CASCADE => true, Relation::INNER_KEY => 'id', Relation::OUTER_KEY => 'id', Relation::THROUGH_ENTITY => 'post_tag', Relation::THROUGH_INNER_KEY => 'post_id', Relation::THROUGH_OUTER_KEY => 'tag_id', ], ], ], ], ]); // Access schema information $roles = $schema->getRoles(); // ['user', 'profile', 'post', ...] $relations = $schema->getRelations('user'); // ['profile', 'posts'] $table = $schema->define('user', SchemaInterface::TABLE); // 'users' ``` ### Entity Creation with ORM Create entity instances using the ORM factory with optional data hydration. ```php make(User::class); $user->name = 'Jane Doe'; $user->email = 'jane@example.com'; // Create with initial data $user = $orm->make(User::class, [ 'name' => 'Jane Doe', 'email' => 'jane@example.com', 'balance' => 500.00, ]); // Create as managed entity (tracked in heap) $user = $orm->make(User::class, [ 'id' => 1, 'name' => 'Jane Doe', 'email' => 'jane@example.com', ], Node::MANAGED); // Create with typecast (for raw database data) $user = $orm->make(User::class, [ 'id' => '1', // Will be cast to int 'balance' => '500.00', // Will be cast to float 'created_at' => '2024-01-01 00:00:00', // Will be cast to DateTime ], Node::MANAGED, typecast: true); ``` ### Working with Relations Manage entity relations through property assignment and cascading persistence. ```php name = 'John Doe'; $user->email = 'john@example.com'; $profile = new Profile(); $profile->bio = 'Developer'; $profile->avatar = 'avatar.jpg'; $user->profile = $profile; $em->persist($user); $em->run(); // Both user and profile are saved // Create user with posts (has-many) $user = new User(); $user->name = 'Jane Doe'; $user->posts = []; $post1 = new Post(); $post1->title = 'First Post'; $post1->content = 'Content here'; $post2 = new Post(); $post2->title = 'Second Post'; $post2->content = 'More content'; $user->posts[] = $post1; $user->posts[] = $post2; $em->persist($user); $em->run(); // User and all posts are saved // Many-to-many relations $post = new Post(); $post->title = 'Tagged Post'; $post->tags = []; $tag1 = $orm->getRepository(Tag::class)->findOne(['name' => 'PHP']); $tag2 = new Tag(); $tag2->name = 'ORM'; $post->tags[] = $tag1; $post->tags[] = $tag2; $em->persist($post); $em->run(); // Post, new tag, and pivot records are saved // Remove from relation $user = $orm->getRepository(User::class) ->select() ->load('posts') ->wherePK(1) ->fetchOne(); // Remove specific post $postToRemove = $user->posts[0]; unset($user->posts[array_search($postToRemove, $user->posts)]); $em->persist($user); $em->delete($postToRemove); // Optional: also delete the post $em->run(); ``` ### Heap and Identity Map The Heap maintains an identity map of loaded entities to prevent duplicate instances. ```php getHeap(); // Check if entity is tracked $isTracked = $heap->has($user); // Get node for entity (contains state and metadata) $node = $heap->get($user); if ($node !== null) { $role = $node->getRole(); $status = $node->getStatus(); // Node::NEW, Node::MANAGED, Node::DELETED $data = $node->getData(); } // Find entity in heap by primary key $user = $heap->find('user', ['id' => 1]); // Clean heap (detach all entities) $heap->clean(); // Create ORM with fresh heap $freshOrm = $orm->with(heap: new \Cycle\ORM\Heap\Heap()); // Detach specific entity $heap->detach($user); ``` ### Custom Repository Create custom repositories with domain-specific query methods. ```php select() ->where('status', 'active') ->orderBy('name', 'ASC') ->fetchAll(); } public function findByEmail(string $email): ?User { return $this->select() ->where('email', $email) ->fetchOne(); } public function findWithRecentOrders(int $days = 30): iterable { return $this->select() ->distinct() ->with('orders') ->where('orders.created_at', '>', new \DateTime("-{$days} days")) ->load('orders', [ 'where' => ['{@}.created_at' => ['>' => new \DateTime("-{$days} days")]] ]) ->fetchAll(); } public function countByStatus(string $status): int { return $this->select() ->where('status', $status) ->count(); } } // Register in schema $schema = new Schema([ 'user' => [ SchemaInterface::ENTITY => User::class, SchemaInterface::REPOSITORY => UserRepository::class, // ... other options ], ]); // Use custom repository /** @var UserRepository $userRepo */ $userRepo = $orm->getRepository(User::class); $activeUsers = $userRepo->findActiveUsers(); $user = $userRepo->findByEmail('john@example.com'); ``` ### Scopes (Global Query Constraints) Apply global query constraints using scopes. ```php where('deleted_at', null); } } class TenantScope implements ScopeInterface { public function __construct( private int $tenantId ) {} public function apply(QueryBuilder $query): void { $query->where('tenant_id', $this->tenantId); } } // Register scope in schema $schema = new Schema([ 'user' => [ SchemaInterface::ENTITY => User::class, SchemaInterface::SCOPE => ActiveScope::class, // ... other options ], ]); // Or apply scope at runtime $users = (new Select($orm, User::class)) ->scope(new TenantScope(tenantId: 42)) ->fetchAll(); // Remove scope for query $allUsers = (new Select($orm, User::class)) ->scope(null) ->fetchAll(); ``` ### Collection Factories Configure custom collection types for has-many and many-to-many relations. ```php withCollectionFactory( 'doctrine', new DoctrineCollectionFactory(), ArrayCollection::class ); // Use Laravel/Illuminate Collections $factory = (new Factory($dbal)) ->withCollectionFactory( 'illuminate', new IlluminateCollectionFactory(), Collection::class ); // Specify collection type in schema $schema = new Schema([ 'user' => [ // ... other options SchemaInterface::RELATIONS => [ 'posts' => [ Relation::TYPE => Relation::HAS_MANY, Relation::TARGET => 'post', Relation::COLLECTION_TYPE => 'doctrine', // or 'illuminate' Relation::SCHEMA => [ // ... ], ], ], ], ]); ``` ## Summary Cycle ORM is well-suited for building data access layers in PHP applications that require complex entity relationships, efficient query building, and safe persistence patterns. Common use cases include web applications with complex domain models, API backends requiring optimized database access, long-running worker processes that benefit from the immutable service core, and applications using modern PHP frameworks like Spiral or custom setups with RoadRunner. Integration typically involves configuring the DatabaseManager with your database connections, defining entity schemas (either programmatically or using the schema-builder extension with annotations), creating the ORM instance, and using repositories and the EntityManager for data access. The architecture supports dependency injection, making it easy to integrate with PSR-11 containers. For large applications, consider using the schema-provider extension to cache compiled schemas and the annotated extension to define schemas using PHP 8 attributes on your entity classes.