Cycle ORM

Cycle ORMは、PHP向けの次世代型Object-Relational Mapping(ORM)ライブラリで、従来のActiveRecordパターンとDataMapperパターンの両方をサポートする柔軟な設計が特徴です。強力なスキーマビルダー、自動マイグレーション、関連性マッピング、クエリビルダー機能を統合し、SQLインジェクション防止やデータ整合性の確保を自動化します。MySQL、PostgreSQL、SQLite、SQLServerなど主要データベースに対応し、高性能なデータアクセス層の構築を実現する現代的なPHP ORMソリューションです。

ORMPHPActiveRecordDataMapperSchemaBuilderマイグレーション

GitHub概要

cycle/orm

PHP DataMapper, ORM

ホームページ:https://cycle-orm.dev
スター1,255
ウォッチ27
フォーク78
作成日:2018年11月7日
言語:PHP
ライセンス:MIT License

トピックス

cycledatamapperhacktoberfestmapperormphpquery-builder

スター履歴

cycle/orm Star History
データ取得日時: 2025/7/17 10:32

ライブラリ

Cycle ORM

概要

Cycle ORMは、PHP向けの次世代型Object-Relational Mapping(ORM)ライブラリで、従来のActiveRecordパターンとDataMapperパターンの両方をサポートする柔軟な設計が特徴です。強力なスキーマビルダー、自動マイグレーション、関連性マッピング、クエリビルダー機能を統合し、SQLインジェクション防止やデータ整合性の確保を自動化します。MySQL、PostgreSQL、SQLite、SQLServerなど主要データベースに対応し、高性能なデータアクセス層の構築を実現する現代的なPHP ORMソリューションです。

詳細

Cycle ORM 2025年版は、PHPのモダンな機能(PHP 8.1+)を最大限活用し、型安全性とパフォーマンスを両立した設計になっています。コンパイル時のスキーマ解析により実行時のオーバーヘッドを最小化し、レイジーローディング、イーガーローディング、バッチ処理など効率的なデータ取得戦略を提供します。Annotation/Attributeベースのエンティティ定義、クエリビルダーとRAW SQLの混在利用、高度なリレーションシップマッピング(1:1、1:多、多:多)、カスタムタイプキャスティング機能により、複雑なビジネスロジックを簡潔に表現できます。

主な特徴

  • デュアルパターンサポート: ActiveRecordとDataMapperの両方に対応
  • 型安全なスキーマ: PHP Attributeによる型安全なエンティティ定義
  • 高度なクエリビルダー: 直感的で強力なクエリ構築機能
  • 自動マイグレーション: スキーマ変更の自動検出と適用
  • リレーションシップ: 複雑な関連性を簡単に定義・操作
  • パフォーマンス最適化: レイジーロード、バッチ処理、キャッシング

メリット・デメリット

メリット

  • ActiveRecordとDataMapperの柔軟な選択により多様な開発スタイルに対応
  • 型安全なスキーマ定義によりIDEサポートとバグ予防を実現
  • 自動マイグレーション機能により開発効率が大幅向上
  • 高性能なクエリ実行とメモリ効率的なデータ処理
  • PostgreSQL、MySQL、SQLite等の主要データベースを統一的に操作
  • 豊富なリレーションシップサポートで複雑なデータ構造も簡潔に表現

デメリット

  • 学習曲線が急で、小規模プロジェクトには過剰な場合がある
  • EloquentやDoctrineと比較して日本語情報やコミュニティが限定的
  • 複雑な設定が必要で、初期セットアップのコストが高い
  • デバッグ時にORMの内部動作を理解する必要がある場面がある
  • パフォーマンスチューニングには専門知識が必要
  • サードパーティ統合ツールやプラグインが他のORMより少ない

参考ページ

書き方の例

基本セットアップ

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

// データベース接続の設定
$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',
            ),
        ),
    ]
]));

// スキーマプロバイダーとORM初期化
$schema = new ORM\Schema\GeneratedSchemaProvider();
$orm = new ORM\ORM(new ORM\Factory($dbal), $schema);

echo "Cycle ORM初期化完了" . PHP_EOL;

モデル定義と基本操作

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

// ユーザーエンティティの定義
#[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();
    }
}

// 基本的なCRUD操作
class UserService
{
    private ORM\ORMInterface $orm;

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

    // ユーザー作成
    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;
    }

    // ユーザー取得
    public function getUserById(int $id): ?User
    {
        $repository = $this->orm->getRepository(User::class);
        return $repository->findByPK($id);
    }

    // ユーザー一覧取得
    public function getAllUsers(): array
    {
        $repository = $this->orm->getRepository(User::class);
        return $repository->findAll();
    }

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

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

    // 条件付き検索
    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();
    }
}

高度なクエリ操作

<?php

class AdvancedUserQueries
{
    private ORM\ORMInterface $orm;

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

    // 複雑な条件でのクエリ
    public function searchUsers(array $criteria): array
    {
        $repository = $this->orm->getRepository(User::class);
        $query = $repository->select();

        // 動的フィルタリング
        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();
    }

    // ページネーション
    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)
        ];
    }

    // 集約クエリ
    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();

        // 年代別統計
        $ageGroups = $db->select([
                'CASE 
                    WHEN age < 20 THEN "10代" 
                    WHEN age < 30 THEN "20代"
                    WHEN age < 40 THEN "30代"
                    WHEN age < 50 THEN "40代"
                    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クエリの実行
    public function executeCustomQuery(string $sql, array $params = []): array
    {
        $db = $this->orm->getDatabase();
        return $db->query($sql, $params)->fetchAll();
    }

    // バルク操作
    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();
    }
}

リレーション操作

<?php

// 投稿エンティティ
#[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;

    // User との関連(多対一)
    #[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();
    }
}

// タグエンティティ
#[Entity(table: 'tags')]
class Tag
{
    #[Column(type: 'primary')]
    public int $id;

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

    // Post との多対多関連
    #[Cycle\Annotated\Annotation\Relation\ManyToMany(target: Post::class, through: 'post_tags')]
    public array $posts = [];

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

// ユーザーエンティティに投稿の関連を追加
class User // 拡張版
{
    // ... 既存のプロパティ

    // Post との関連(一対多)
    #[Cycle\Annotated\Annotation\Relation\HasMany(target: Post::class)]
    public array $posts = [];

    // ... 既存のメソッド
}

// リレーション操作のサービス
class PostService
{
    private ORM\ORMInterface $orm;

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

    // 投稿をタグ付きで作成
    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);

        // タグの取得または作成
        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;
    }

    // ユーザーの投稿を関連データと一緒に取得
    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();
    }

    // 公開投稿をユーザー情報付きで取得
    public function getPublishedPostsWithAuthors(): array
    {
        $repository = $this->orm->getRepository(Post::class);

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

    // 特定タグの投稿を取得
    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();
    }

    // 投稿数の多いユーザーTOP10
    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();
    }
}

実用例

<?php

// 実用的なアプリケーション例
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);
    }

    // ブログの初期データセットアップ
    public function setupBlogData(): void
    {
        // サンプルユーザーの作成
        $users = [
            ['田中太郎', '[email protected]', 30],
            ['佐藤花子', '[email protected]', 25],
            ['鈴木一郎', '[email protected]', 35]
        ];

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

        // サンプル投稿の作成
        $posts = [
            ['PHP 8.2の新機能', 'PHP 8.2で追加された新機能について解説します...', ['PHP', 'Web開発']],
            ['Cycle ORMの活用法', 'Cycle ORMを効果的に使う方法を紹介します...', ['PHP', 'ORM', 'データベース']],
            ['モダンPHP開発', '現代的なPHP開発のベストプラクティス...', ['PHP', 'ベストプラクティス']]
        ];

        foreach ($posts as $index => [$title, $content, $tags]) {
            $user = $createdUsers[$index % count($createdUsers)];
            $post = $this->postService->createPostWithTags($title, $content, $user, $tags);
            
            // いくつかの投稿を公開状態にする
            if ($index % 2 === 0) {
                $post->published = true;
                $manager = new ORM\EntityManager($this->orm);
                $manager->persist($post);
                $manager->run();
            }
        }

        echo "ブログデータのセットアップが完了しました" . PHP_EOL;
    }

    // ダッシュボード情報の取得
    public function getDashboardData(): array
    {
        $userStats = (new AdvancedUserQueries($this->orm))->getUserStatistics();
        $topUsers = $this->postService->getTopActiveUsers(5);
        $recentPosts = $this->postService->getPublishedPostsWithAuthors();

        // 投稿統計
        $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)
        ];
    }

    // トランザクション処理の例
    public function transferPostOwnership(int $postId, int $newUserId): bool
    {
        try {
            $manager = new ORM\EntityManager($this->orm);
            
            // トランザクション開始
            $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);

            // 所有権移転ログの記録(仮想的な例)
            $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("投稿所有権移転エラー: " . $e->getMessage());
            return false;
        }
    }

    // キャッシュ機能付きデータ取得
    public function getCachedPopularPosts(int $limit = 10): array
    {
        $cacheKey = "popular_posts_{$limit}";
        
        // PSR-16互換のキャッシュライブラリを使用することを想定
        // $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' // 仮想的なコメント数
            ])
            ->from('posts', 'p')
            ->join('users', 'u')->on('p.user_id', 'u.id')
            ->leftJoin('comments', 'c')->on('p.id', 'c.post_id') // 仮想的なコメントテーブル
            ->where('p.published', true)
            ->groupBy('p.id', 'u.name')
            ->orderBy('comment_count', 'DESC')
            ->limit($limit)
            ->fetchAll();

        // $cache->set($cacheKey, $popularPosts, 3600); // 1時間キャッシュ

        return $popularPosts;
    }

    // レポート生成
    public function generateMonthlyReport(\DateTime $month): array
    {
        $startDate = $month->format('Y-m-01');
        $endDate = $month->format('Y-m-t');

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

        // 月間新規ユーザー数
        $newUsers = $db->select('COUNT(*) as count')
            ->from('users')
            ->where('createdAt', '>=', $startDate)
            ->where('createdAt', '<=', $endDate . ' 23:59:59')
            ->fetchOne();

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

        // 日別アクティビティ
        $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('Y年m月'),
            'new_users' => $newUsers['count'],
            'new_posts' => $newPosts['count'],
            'daily_activity' => $dailyActivity
        ];
    }
}

// アプリケーションの実行例
$app = new BlogApplication($orm);
$app->setupBlogData();

$dashboard = $app->getDashboardData();
echo "=== ダッシュボード ===" . PHP_EOL;
echo "総ユーザー数: " . $dashboard['user_stats']['overall']['total'] . PHP_EOL;
echo "総投稿数: " . $dashboard['post_stats']['total_posts'] . PHP_EOL;
echo "公開投稿数: " . $dashboard['post_stats']['published_posts'] . PHP_EOL;

$report = $app->generateMonthlyReport(new \DateTime());
echo "\n=== 月間レポート ===" . PHP_EOL;
echo "期間: " . $report['period'] . PHP_EOL;
echo "新規ユーザー: " . $report['new_users'] . "人" . PHP_EOL;
echo "新規投稿: " . $report['new_posts'] . "件" . PHP_EOL;