Cycle ORM

Cycle ORM is a next-generation Object-Relational Mapping (ORM) library for PHP, featuring flexible design that supports both traditional ActiveRecord and DataMapper patterns. It integrates powerful schema builder, automatic migration, relationship mapping, and query builder functionality, while automating SQL injection prevention and data integrity assurance. Supporting major databases including MySQL, PostgreSQL, SQLite, and SQL Server, it provides a modern PHP ORM solution for building high-performance data access layers.

ORMPHPActiveRecordDataMapperSchemaBuilderMigration

GitHub Overview

cycle/orm

PHP DataMapper, ORM

Stars1,255
Watchers27
Forks78
Created:November 7, 2018
Language:PHP
License:MIT License

Topics

cycledatamapperhacktoberfestmapperormphpquery-builder

Star History

cycle/orm Star History
Data as of: 7/17/2025, 10:32 AM

Library

Cycle ORM

Overview

Cycle ORM is a next-generation Object-Relational Mapping (ORM) library for PHP, featuring flexible design that supports both traditional ActiveRecord and DataMapper patterns. It integrates powerful schema builder, automatic migration, relationship mapping, and query builder functionality, while automating SQL injection prevention and data integrity assurance. Supporting major databases including MySQL, PostgreSQL, SQLite, and SQL Server, it provides a modern PHP ORM solution for building high-performance data access layers.

Details

Cycle ORM 2025 edition maximizes modern PHP features (PHP 8.1+) with a design that balances type safety and performance. It minimizes runtime overhead through compile-time schema analysis and provides efficient data retrieval strategies including lazy loading, eager loading, and batch processing. With annotation/attribute-based entity definition, mixed use of query builder and raw SQL, advanced relationship mapping (1:1, 1:many, many:many), and custom type casting functionality, it enables concise expression of complex business logic.

Key Features

  • Dual Pattern Support: Supports both ActiveRecord and DataMapper patterns
  • Type-safe Schema: Type-safe entity definition using PHP Attributes
  • Advanced Query Builder: Intuitive and powerful query construction functionality
  • Automatic Migrations: Automatic detection and application of schema changes
  • Relationships: Easy definition and manipulation of complex associations
  • Performance Optimization: Lazy loading, batch processing, and caching

Pros and Cons

Pros

  • Flexible choice between ActiveRecord and DataMapper accommodates diverse development styles
  • Type-safe schema definition enables IDE support and bug prevention
  • Automatic migration functionality significantly improves development efficiency
  • High-performance query execution and memory-efficient data processing
  • Unified operation of major databases including PostgreSQL, MySQL, SQLite
  • Rich relationship support allows concise expression of complex data structures

Cons

  • Steep learning curve, may be excessive for small projects
  • Limited Japanese resources and community compared to Eloquent or Doctrine
  • Requires complex configuration with high initial setup costs
  • May need understanding of ORM internals during debugging
  • Performance tuning requires specialized knowledge
  • Fewer third-party integration tools and plugins compared to other ORMs

Reference Pages

Code Examples

Basic Setup

<?php
use Cycle\ORM;
use Cycle\Database;

// Database connection configuration
$dbal = new Database\DatabaseManager(new Database\Config\DatabaseConfig([
    'default' => 'default',
    'databases' => [
        'default' => [
            'connection' => 'mysql'
        ]
    ],
    'connections' => [
        'mysql' => new Database\Config\MySQLDriverConfig(
            connection: new Database\Config\MySQL\TcpConnectionConfig(
                database: 'test_db',
                host: '127.0.0.1',
                port: 3306,
                user: 'root',
                password: 'password',
            ),
        ),
    ]
]));

// Schema provider and ORM initialization
$schema = new ORM\Schema\GeneratedSchemaProvider();
$orm = new ORM\ORM(new ORM\Factory($dbal), $schema);

echo "Cycle ORM initialization completed" . PHP_EOL;

Model Definition and Basic Operations

<?php
use Cycle\Annotated\Annotation\Column;
use Cycle\Annotated\Annotation\Entity;
use Cycle\ORM\Entity\Behavior;

// User entity definition
#[Entity(table: 'users')]
#[Behavior\CreatedAt(field: 'createdAt')]
#[Behavior\UpdatedAt(field: 'updatedAt')]
class User
{
    #[Column(type: 'primary')]
    public int $id;

    #[Column(type: 'string', length: 64)]
    public string $name;

    #[Column(type: 'string', unique: true)]
    public string $email;

    #[Column(type: 'int')]
    public int $age;

    #[Column(type: 'datetime')]
    public \DateTimeImmutable $createdAt;

    #[Column(type: 'datetime', nullable: true)]
    public ?\DateTimeImmutable $updatedAt = null;

    public function __construct(string $name, string $email, int $age)
    {
        $this->name = $name;
        $this->email = $email;
        $this->age = $age;
        $this->createdAt = new \DateTimeImmutable();
    }
}

// Basic CRUD operations
class UserService
{
    private ORM\ORMInterface $orm;

    public function __construct(ORM\ORMInterface $orm)
    {
        $this->orm = $orm;
    }

    // Create user
    public function createUser(string $name, string $email, int $age): User
    {
        $user = new User($name, $email, $age);

        $manager = new ORM\EntityManager($this->orm);
        $manager->persist($user);
        $manager->run();

        return $user;
    }

    // Get user
    public function getUserById(int $id): ?User
    {
        $repository = $this->orm->getRepository(User::class);
        return $repository->findByPK($id);
    }

    // Get all users
    public function getAllUsers(): array
    {
        $repository = $this->orm->getRepository(User::class);
        return $repository->findAll();
    }

    // Update user
    public function updateUser(User $user): void
    {
        $manager = new ORM\EntityManager($this->orm);
        $manager->persist($user);
        $manager->run();
    }

    // Delete user
    public function deleteUser(User $user): void
    {
        $manager = new ORM\EntityManager($this->orm);
        $manager->delete($user);
        $manager->run();
    }

    // Conditional search
    public function getUsersByAge(int $minAge, int $maxAge): array
    {
        $repository = $this->orm->getRepository(User::class);
        return $repository->select()
            ->where('age', '>=', $minAge)
            ->where('age', '<=', $maxAge)
            ->orderBy('age')
            ->fetchAll();
    }
}

Advanced Query Operations

<?php

class AdvancedUserQueries
{
    private ORM\ORMInterface $orm;

    public function __construct(ORM\ORMInterface $orm)
    {
        $this->orm = $orm;
    }

    // Complex conditional queries
    public function searchUsers(array $criteria): array
    {
        $repository = $this->orm->getRepository(User::class);
        $query = $repository->select();

        // Dynamic filtering
        if (!empty($criteria['name'])) {
            $query->where('name', 'LIKE', "%{$criteria['name']}%");
        }

        if (!empty($criteria['email_domain'])) {
            $query->where('email', 'LIKE', "%@{$criteria['email_domain']}");
        }

        if (!empty($criteria['age_range'])) {
            [$min, $max] = $criteria['age_range'];
            $query->where('age', 'BETWEEN', $min, $max);
        }

        if (!empty($criteria['created_after'])) {
            $query->where('createdAt', '>', $criteria['created_after']);
        }

        return $query->orderBy('createdAt', 'DESC')->fetchAll();
    }

    // Pagination
    public function getUsersPaginated(int $page, int $limit): array
    {
        $repository = $this->orm->getRepository(User::class);
        $offset = ($page - 1) * $limit;

        $users = $repository->select()
            ->limit($limit)
            ->offset($offset)
            ->orderBy('id')
            ->fetchAll();

        $total = $repository->select()->count();

        return [
            'users' => $users,
            'total' => $total,
            'page' => $page,
            'pages' => ceil($total / $limit)
        ];
    }

    // Aggregate queries
    public function getUserStatistics(): array
    {
        $db = $this->orm->getDatabase();

        $stats = $db->select('COUNT(*) as total, AVG(age) as avg_age, MIN(age) as min_age, MAX(age) as max_age')
            ->from('users')
            ->fetchOne();

        // Age group statistics
        $ageGroups = $db->select([
                'CASE 
                    WHEN age < 20 THEN "Teens" 
                    WHEN age < 30 THEN "20s"
                    WHEN age < 40 THEN "30s"
                    WHEN age < 50 THEN "40s"
                    ELSE "50+"
                END as age_group',
                'COUNT(*) as count'
            ])
            ->from('users')
            ->groupBy('age_group')
            ->orderBy('age_group')
            ->fetchAll();

        return [
            'overall' => $stats,
            'age_groups' => $ageGroups
        ];
    }

    // Raw SQL query execution
    public function executeCustomQuery(string $sql, array $params = []): array
    {
        $db = $this->orm->getDatabase();
        return $db->query($sql, $params)->fetchAll();
    }

    // Bulk operations
    public function bulkUpdateUserAges(array $updates): void
    {
        $manager = new ORM\EntityManager($this->orm);

        foreach ($updates as $update) {
            $user = $this->orm->getRepository(User::class)->findByPK($update['id']);
            if ($user) {
                $user->age = $update['age'];
                $manager->persist($user);
            }
        }

        $manager->run();
    }
}

Relationship Operations

<?php

// Post entity
#[Entity(table: 'posts')]
#[Behavior\CreatedAt(field: 'createdAt')]
class Post
{
    #[Column(type: 'primary')]
    public int $id;

    #[Column(type: 'string')]
    public string $title;

    #[Column(type: 'text')]
    public string $content;

    #[Column(type: 'bool', default: false)]
    public bool $published = false;

    #[Column(type: 'datetime')]
    public \DateTimeImmutable $createdAt;

    // Relationship with User (many-to-one)
    #[Cycle\Annotated\Annotation\Relation\BelongsTo(target: User::class)]
    public User $user;

    public function __construct(string $title, string $content, User $user)
    {
        $this->title = $title;
        $this->content = $content;
        $this->user = $user;
        $this->createdAt = new \DateTimeImmutable();
    }
}

// Tag entity
#[Entity(table: 'tags')]
class Tag
{
    #[Column(type: 'primary')]
    public int $id;

    #[Column(type: 'string', unique: true)]
    public string $name;

    // Many-to-many relationship with Post
    #[Cycle\Annotated\Annotation\Relation\ManyToMany(target: Post::class, through: 'post_tags')]
    public array $posts = [];

    public function __construct(string $name)
    {
        $this->name = $name;
    }
}

// Extended User entity with posts relationship
class User // Extended version
{
    // ... existing properties

    // Relationship with Post (one-to-many)
    #[Cycle\Annotated\Annotation\Relation\HasMany(target: Post::class)]
    public array $posts = [];

    // ... existing methods
}

// Service for relationship operations
class PostService
{
    private ORM\ORMInterface $orm;

    public function __construct(ORM\ORMInterface $orm)
    {
        $this->orm = $orm;
    }

    // Create post with tags
    public function createPostWithTags(string $title, string $content, User $user, array $tagNames): Post
    {
        $post = new Post($title, $content, $user);
        
        $manager = new ORM\EntityManager($this->orm);
        $tagRepo = $this->orm->getRepository(Tag::class);

        // Get or create tags
        foreach ($tagNames as $tagName) {
            $tag = $tagRepo->findOne(['name' => $tagName]);
            if (!$tag) {
                $tag = new Tag($tagName);
                $manager->persist($tag);
            }
            $post->tags[] = $tag;
        }

        $manager->persist($post);
        $manager->run();

        return $post;
    }

    // Get user posts with related data
    public function getUserPostsWithTags(int $userId): array
    {
        $repository = $this->orm->getRepository(Post::class);
        
        return $repository->select()
            ->load('user')
            ->load('tags')
            ->where('user.id', $userId)
            ->orderBy('createdAt', 'DESC')
            ->fetchAll();
    }

    // Get published posts with author information
    public function getPublishedPostsWithAuthors(): array
    {
        $repository = $this->orm->getRepository(Post::class);

        return $repository->select()
            ->with('user')
            ->where('published', true)
            ->orderBy('createdAt', 'DESC')
            ->fetchAll();
    }

    // Get posts by specific tag
    public function getPostsByTag(string $tagName): array
    {
        $repository = $this->orm->getRepository(Post::class);

        return $repository->select()
            ->load('user')
            ->load('tags')
            ->with('tags', ['where' => ['name' => $tagName]])
            ->fetchAll();
    }

    // Top 10 users with most posts
    public function getTopActiveUsers(int $limit = 10): array
    {
        $db = $this->orm->getDatabase();

        return $db->select([
                'u.id',
                'u.name',
                'u.email',
                'COUNT(p.id) as post_count'
            ])
            ->from('users', 'u')
            ->leftJoin('posts', 'p')->on('u.id', 'p.user_id')
            ->groupBy('u.id', 'u.name', 'u.email')
            ->orderBy('post_count', 'DESC')
            ->limit($limit)
            ->fetchAll();
    }
}

Practical Examples

<?php

// Practical application example
class BlogApplication
{
    private ORM\ORMInterface $orm;
    private UserService $userService;
    private PostService $postService;

    public function __construct(ORM\ORMInterface $orm)
    {
        $this->orm = $orm;
        $this->userService = new UserService($orm);
        $this->postService = new PostService($orm);
    }

    // Setup initial blog data
    public function setupBlogData(): void
    {
        // Create sample users
        $users = [
            ['John Doe', '[email protected]', 30],
            ['Jane Smith', '[email protected]', 25],
            ['Bob Johnson', '[email protected]', 35]
        ];

        $createdUsers = [];
        foreach ($users as [$name, $email, $age]) {
            $createdUsers[] = $this->userService->createUser($name, $email, $age);
        }

        // Create sample posts
        $posts = [
            ['PHP 8.2 New Features', 'Explaining new features added in PHP 8.2...', ['PHP', 'Web Development']],
            ['Cycle ORM Usage', 'How to effectively use Cycle ORM...', ['PHP', 'ORM', 'Database']],
            ['Modern PHP Development', 'Best practices for modern PHP development...', ['PHP', 'Best Practices']]
        ];

        foreach ($posts as $index => [$title, $content, $tags]) {
            $user = $createdUsers[$index % count($createdUsers)];
            $post = $this->postService->createPostWithTags($title, $content, $user, $tags);
            
            // Publish some posts
            if ($index % 2 === 0) {
                $post->published = true;
                $manager = new ORM\EntityManager($this->orm);
                $manager->persist($post);
                $manager->run();
            }
        }

        echo "Blog data setup completed" . PHP_EOL;
    }

    // Get dashboard information
    public function getDashboardData(): array
    {
        $userStats = (new AdvancedUserQueries($this->orm))->getUserStatistics();
        $topUsers = $this->postService->getTopActiveUsers(5);
        $recentPosts = $this->postService->getPublishedPostsWithAuthors();

        // Post statistics
        $db = $this->orm->getDatabase();
        $postStats = $db->select([
                'COUNT(*) as total_posts',
                'COUNT(CASE WHEN published = 1 THEN 1 END) as published_posts',
                'COUNT(CASE WHEN published = 0 THEN 1 END) as draft_posts'
            ])
            ->from('posts')
            ->fetchOne();

        return [
            'user_stats' => $userStats,
            'post_stats' => $postStats,
            'top_users' => $topUsers,
            'recent_posts' => array_slice($recentPosts, 0, 5)
        ];
    }

    // Transaction processing example
    public function transferPostOwnership(int $postId, int $newUserId): bool
    {
        try {
            $manager = new ORM\EntityManager($this->orm);
            
            // Begin transaction
            $transaction = $this->orm->getDatabase()->transaction();
            
            $post = $this->orm->getRepository(Post::class)->findByPK($postId);
            $newUser = $this->orm->getRepository(User::class)->findByPK($newUserId);
            
            if (!$post || !$newUser) {
                $transaction->rollback();
                return false;
            }

            $oldUserId = $post->user->id;
            $post->user = $newUser;
            $manager->persist($post);

            // Log ownership transfer (hypothetical example)
            $db = $this->orm->getDatabase();
            $db->insert('ownership_transfers')
                ->values([
                    'post_id' => $postId,
                    'old_user_id' => $oldUserId,
                    'new_user_id' => $newUserId,
                    'transferred_at' => new \DateTimeImmutable()
                ])
                ->run();

            $manager->run();
            $transaction->commit();

            return true;

        } catch (\Exception $e) {
            $transaction->rollback();
            error_log("Post ownership transfer error: " . $e->getMessage());
            return false;
        }
    }

    // Cached data retrieval
    public function getCachedPopularPosts(int $limit = 10): array
    {
        $cacheKey = "popular_posts_{$limit}";
        
        // Assuming use of PSR-16 compatible cache library
        // $cache = new SomeCache();
        
        /*
        if ($cache->has($cacheKey)) {
            return $cache->get($cacheKey);
        }
        */

        $db = $this->orm->getDatabase();
        $popularPosts = $db->select([
                'p.*',
                'u.name as author_name',
                'COUNT(c.id) as comment_count' // Hypothetical comment count
            ])
            ->from('posts', 'p')
            ->join('users', 'u')->on('p.user_id', 'u.id')
            ->leftJoin('comments', 'c')->on('p.id', 'c.post_id') // Hypothetical comments table
            ->where('p.published', true)
            ->groupBy('p.id', 'u.name')
            ->orderBy('comment_count', 'DESC')
            ->limit($limit)
            ->fetchAll();

        // $cache->set($cacheKey, $popularPosts, 3600); // 1 hour cache

        return $popularPosts;
    }

    // Report generation
    public function generateMonthlyReport(\DateTime $month): array
    {
        $startDate = $month->format('Y-m-01');
        $endDate = $month->format('Y-m-t');

        $db = $this->orm->getDatabase();

        // Monthly new users count
        $newUsers = $db->select('COUNT(*) as count')
            ->from('users')
            ->where('createdAt', '>=', $startDate)
            ->where('createdAt', '<=', $endDate . ' 23:59:59')
            ->fetchOne();

        // Monthly posts count
        $newPosts = $db->select('COUNT(*) as count')
            ->from('posts')
            ->where('createdAt', '>=', $startDate)
            ->where('createdAt', '<=', $endDate . ' 23:59:59')
            ->fetchOne();

        // Daily activity
        $dailyActivity = $db->select([
                'DATE(createdAt) as date',
                'COUNT(*) as post_count'
            ])
            ->from('posts')
            ->where('createdAt', '>=', $startDate)
            ->where('createdAt', '<=', $endDate . ' 23:59:59')
            ->groupBy('DATE(createdAt)')
            ->orderBy('date')
            ->fetchAll();

        return [
            'period' => $month->format('F Y'),
            'new_users' => $newUsers['count'],
            'new_posts' => $newPosts['count'],
            'daily_activity' => $dailyActivity
        ];
    }
}

// Application execution example
$app = new BlogApplication($orm);
$app->setupBlogData();

$dashboard = $app->getDashboardData();
echo "=== Dashboard ===" . PHP_EOL;
echo "Total Users: " . $dashboard['user_stats']['overall']['total'] . PHP_EOL;
echo "Total Posts: " . $dashboard['post_stats']['total_posts'] . PHP_EOL;
echo "Published Posts: " . $dashboard['post_stats']['published_posts'] . PHP_EOL;

$report = $app->generateMonthlyReport(new \DateTime());
echo "\n=== Monthly Report ===" . PHP_EOL;
echo "Period: " . $report['period'] . PHP_EOL;
echo "New Users: " . $report['new_users'] . " users" . PHP_EOL;
echo "New Posts: " . $report['new_posts'] . " posts" . PHP_EOL;