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