cycle/schema-builder
Cycle ORM Schema Builder lets you define and configure your ORM and database schema programmatically using annotations or custom generators. Register entities, map fields and columns, link tables, then compile the schema for Cycle ORM.
Install the package:
composer require cycle/schema-builder
Use 2.12.2 or newer if your schema relies on nullable relation-generated foreign keys shared by multiple relations.
Initialize the Registry:
use Cycle\Database;
use Cycle\Schema\Registry;
$dbal = new Database\DatabaseManager(new Config\DatabaseConfig([...]));
$registry = new Registry($dbal);
Define a basic entity:
$entity = (new Definition\Entity())
->setRole('user')
->setClass(User::class);
$entity->getFields()
->set('id', (new Definition\Field())->setType('primary')->setPrimary(true))
->set('name', (new Definition\Field())->setType('string(32)'));
$registry->register($entity);
$registry->linkTable($entity, 'default', 'users');
Compile and apply the schema:
$schema = (new Compiler())->compile($registry);
$orm = $orm->withSchema(new \Cycle\ORM\Schema($schema));
Use this for rapid prototyping or one-off schema changes without writing migrations. Example:
$registry = new Registry($dbal);
$entity = (new Definition\Entity())
->setRole('product')
->setClass(Product::class);
$entity->getFields()
->set('id', (new Definition\Field())->setType('primary')->setPrimary(true))
->set('sku', (new Definition\Field())->setType('string(255)')->setUnique(true))
->set('price', (new Definition\Field())->setType('decimal(10,2)'));
$registry->register($entity);
$registry->linkTable($entity, 'default', 'products');
// Generate and apply schema
$schema = (new Compiler())->compile($registry);
$orm->withSchema(new \Cycle\ORM\Schema($schema));
Define Entities:
Use Definition\Entity to map PHP classes to database tables. Assign a role (e.g., 'user') for schema resolution.
$entity = (new Definition\Entity())
->setRole('post')
->setClass(Post::class);
Configure Fields:
Define columns with Definition\Field, specifying types (string, int, decimal), constraints (primary, unique), and attributes (e.g., default, nullable).
$entity->getFields()
->set('title', (new Definition\Field())->setType('string(255)')->setNullable(false))
->set('published_at', (new Definition\Field())->setType('datetime')->setDefault('now()'));
Link Tables: Associate entities with database connections and table names.
$registry->linkTable($entity, 'default', 'posts');
Compile and Apply:
Use Compiler to generate the schema and integrate it with Cycle ORM.
$schema = (new Compiler())->compile($registry);
$orm->withSchema(new \Cycle\ORM\Schema($schema));
Define foreign keys and relationships declaratively:
$authorEntity = (new Definition\Entity())->setRole('author')->setClass(Author::class);
$authorEntity->getFields()->set('id', (new Definition\Field())->setType('primary')->setPrimary(true));
$postEntity->getFields()
->set('author_id', (new Definition\Field())
->setType('integer')
->setForeignKey('author_id', $authorEntity, 'id')
->setOnDelete('cascade')
);
When several relations share the same generated foreign key column, nullability is inferred from those relations. As of 2.12.2, if any relation sharing that FK asks for nullable, the generated FK column is nullable. This fixes cases where one nullable relation could still produce a non-nullable FK column.
Add indexes or constraints programmatically:
$entity->getIndexes()
->add('idx_post_title', 'title', 'unique');
$entity->getConstraints()
->add('chk_post_price', 'price', '>', 0);
Use Definition\Field::setGenerated() for computed columns:
$entity->getFields()
->set('slug', (new Definition\Field())
->setType('string(255)')
->setGenerated('CONCAT(LOWER(title), "-", id)')
);
Extend functionality with custom modifiers (e.g., for soft deletes):
use Cycle\Schema\Modifier;
class SoftDeleteModifier implements Modifier {
public function modify(Definition\Entity $entity): void {
$entity->getFields()->set('deleted_at', (new Definition\Field())
->setType('datetime')
->setNullable(true)
);
}
}
$registry->addModifier(new SoftDeleteModifier());
Generate schemas at runtime (e.g., for multi-tenant apps):
$tenantId = 1;
$tenantSchema = (new Compiler())->compile($registry->forTenant($tenantId));
Use @Cycle\Annotation attributes for schema definitions (alternative to fluent API):
#[Cycle\Entity(table: 'users')]
class User {
#[Cycle\Column(type: 'primary')]
public int $id;
#[Cycle\Column(type: 'string(32)', nullable: false)]
public string $name;
}
Configure the registry to parse annotations:
$registry->addParser(new \Cycle\Annotated\Parser());
Role Conflicts:
'user'). Reusing roles can cause schema resolution errors.'app_user' instead of generic 'user'.Missing Table Links:
$registry->linkTable() will result in unassigned tables.foreach ($registry->getEntities() as $entity) {
if (!$entity->getTable()) {
throw new \RuntimeException("Table not linked for entity {$entity->getRole()}");
}
}
Foreign Key Duplication:
setNoIndex() to avoid this:
->setForeignKey('user_id', $userEntity, 'id')
->setNoIndex(true)
Shared FK Nullability Across Relations:
2.12.2, a nullable relation could still produce a non-nullable FK when another relation shared it.2.12.2+. Remove manual setNullable(true) workarounds if you added them for this case.PHP 8.4+ Deprecations:
v2.12.2) for compatibility.Schema Overwrites:
register() merges existing definitions. Use registerChildWithoutMerge() to force overwrite:
$registry->registerChildWithoutMerge($entity);
Composite Primary Keys:
primary:
$entity->getFields()
->set('user_id', (new Definition\Field())->setType('integer')->setPrimary(true))
->set('order_id', (new Definition\Field())->setType('integer')->setPrimary(true));
Print Schema Changes:
Use the PrintChanges generator to log schema differences:
$generator = new \Cycle\Schema\Generator\PrintChanges();
$generator->generate($registry, $schema);
Validate Schema: Check for errors before compilation:
$validator = new \Cycle\Schema\Validator();
$errors = $validator->validate($registry);
if (!empty($errors)) {
throw new \RuntimeException("Schema validation failed: " . implode(', ', $errors));
}
Inspect Compiled Schema: Dump the compiled schema to debug nullable FK behavior and other generated attributes:
$compiledSchema = (new Compiler())->compile($registry);
file_put_contents('schema_dump.json', json_encode($compiledSchema->toArray(), JSON_PRETTY_PRINT));
Batch Operations: Register multiple entities in a single batch to reduce overhead:
$entities = [
(new Definition\Entity())->setRole('user')->setClass(User::class)->getFields()->set('id', (new Definition\Field())->setType('primary')),
// ... other entities
];
foreach ($entities as $entity) {
$registry->register($entity);
}
Reuse Definitions: Cache compiled schemas if your application has static schemas (e.g., in a monolith):
$schemaCache = new \Symfony\Component\Cache\SimpleFileCache('/tmp/schema_cache');
$schema = $schemaCache->get('orm_schema', function() use ($registry) {
return (new Compiler())->compile($registry);
});
Avoid Redundant Compilation: Compile the schema once (e.g., during bootstrapping) and reuse it.
How can I help you explore Laravel packages today?