sirix / cycle-orm-factory
Cycle ORM Factories for Mezzio
Requires
- php: ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0
- cycle/annotated: ^4.4
- cycle/database: ^2.15
- cycle/orm: ^2.12.3
- psr/container: ^1.0 || ^2.0
- sirix/container-resolver: ^0.2 || ^1.0
- spiral/tokenizer: ^2.14 || ^3.0
Requires (Dev)
- bamarni/composer-bin-plugin: ^1.8
- cycle/entity-behavior: ^1.7
- cycle/migrations: ^4.2
- cycle/schema-migrations-generator: ^2.3
- laminas/laminas-servicemanager: ^3.0 || ^4.0
- mockery/mockery: ^1.6
- phpstan/phpstan-mockery: ^1.1
- phpunit/phpunit: ^11.5
- symfony/console: ^7.3
Suggests
- cycle/entity-behavior: Optional: Entity behavior package for behavior-driven entity mapping
- cycle/entity-behavior-uuid: Optional: UUID behavior extensions for cycle/entity-behavior
- cycle/migrations: Optional: Database migrations runtime (Migrator). Required to run migration commands
- cycle/schema-migrations-generator: Optional: Generate migration files from schema changes
- laminas/laminas-cli: Optional: Provides convenient CLI integration for Laminas/Mezzio applications
- laminas/laminas-servicemanager: Optional: Enables named database services through NamedDatabaseAbstractFactory
- sirix/cycle-orm-extensions: Optional: Practical extensions for Cycle ORM: base repositories, entity traits, typecasts, and more
- symfony/console: Optional: Required only for built-in CLI commands
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-29 06:17:45 UTC
README
Migration guides:
Factories for integrating Cycle ORM into Mezzio with a runtime-focused schema pipeline.
Installation
composer require sirix/cycle-orm-factory
Optional packages:
symfony/console: required for built-in CLI commands.laminas/laminas-cli: optional CLI integration for Mezzio/Laminas.cycle/migrations: required for migration runtime commands.cycle/schema-migrations-generator: required forcycle:schema:migration:generate.cycle/entity-behaviorandcycle/entity-behavior-uuid: optional behavior events; runtime falls back to default Cycle command generator if not installed.laminas/laminas-servicemanager: required for named database services through the abstract factory.
Configuration
Create config/autoload/cycle-orm.global.php:
<?php declare(strict_types=1); use App\Entity\User; use Cycle\Database\Config; use Cycle\ORM\Collection\ArrayCollectionFactory; use Cycle\ORM\Mapper\Mapper; use Cycle\ORM\Relation; use Cycle\ORM\SchemaInterface; return [ 'cycle' => [ 'db-config' => [ 'default' => 'default', 'databases' => [ 'default' => [ 'connection' => 'mysql', ], ], 'connections' => [ 'mysql' => new Config\MySQLDriverConfig( connection: new Config\MySQL\TcpConnectionConfig( database: 'cycle-orm', host: '127.0.0.1', port: 3306, user: 'cycle', password: 'password', ), reconnect: true, timezone: 'UTC', queryCache: true, ), ], ], 'migrator' => [ 'directory' => 'db/migrations', 'table' => 'migrations', 'seed_directory' => 'db/seeds', 'namespace' => 'App\\Migrations', // optional 'vendor_directories' => ['vendor/path'], // optional 'safe' => false, // optional ], 'entities' => [ 'src/App/src/Entity', ], 'generators' => [ // 'my.custom.generator.service', // \App\Cycle\Schema\Generator\MyCustomGenerator::class, // new \App\Cycle\Schema\Generator\InlineGenerator(), ], 'collections' => [ 'default' => ArrayCollectionFactory::class, 'factories' => [ // Register any Cycle ORM collection factory: // 'doctrine' => \Cycle\ORM\Collection\DoctrineCollectionFactory::class, // 'illuminate' => \Cycle\ORM\Collection\IlluminateCollectionFactory::class, // 'loophp' => \Cycle\ORM\Collection\LoophpCollectionFactory::class, ], ], 'schema' => [ 'cache' => [ 'enabled' => true, ], 'compiled' => [ 'path' => 'data/cache/cycle/schema.php', ], 'manual_mapping_schema_definitions' => [ 'user' => [ SchemaInterface::ENTITY => User::class, SchemaInterface::MAPPER => Mapper::class, SchemaInterface::DATABASE => 'default', SchemaInterface::TABLE => 'user', SchemaInterface::PRIMARY_KEY => 'id', SchemaInterface::COLUMNS => [ 'id' => 'id', 'email' => 'email', ], SchemaInterface::TYPECAST => [ 'id' => 'int', ], 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', ], ], ], ], ], ], ], ];
Runtime schema contract (v3)
Runtime behavior is controlled by cycle.schema.cache.enabled.
When true:
- ORM tries to load compiled schema from
cycle.schema.compiled.path. - If file exists: schema is loaded via
requireand ORM is created. - If file is missing: schema is compiled on first start, persisted to file, then reused.
When false:
- Schema is compiled on every start in memory.
- No compiled schema file is read or written by runtime.
Recommended production setup:
- keep
cache.enabled=true - run
cycle:schema:compileduring build/release.
Additional schema generators
cycle.generators supports:
- service ID from container,
- generator FQCN with zero-arg constructor,
- direct instance implementing
Cycle\Schema\GeneratorInterface.
Invalid entries throw Cycle\ORM\Exception\ConfigException.
Collection factories
cycle.collections configures Cycle ORM collection factories for *Many relations.
It works with the built-in Cycle factories and with custom implementations of
Cycle\ORM\Collection\CollectionFactoryInterface.
use Cycle\ORM\Collection\ArrayCollectionFactory; use Cycle\ORM\Collection\DoctrineCollectionFactory; use Cycle\ORM\Collection\IlluminateCollectionFactory; use Cycle\ORM\Collection\LoophpCollectionFactory; return [ 'cycle' => [ 'collections' => [ 'default' => ArrayCollectionFactory::class, 'factories' => [ // Requires doctrine/collections. 'doctrine' => DoctrineCollectionFactory::class, // Requires illuminate/collections. 'illuminate' => IlluminateCollectionFactory::class, // Requires loophp/collection. 'loophp' => LoophpCollectionFactory::class, ], ], ], ];
Factory definitions support:
- service ID from container,
- factory FQCN with zero-arg constructor,
- direct instance implementing
Cycle\ORM\Collection\CollectionFactoryInterface.
Custom collection factories may use the same short form as built-in factories:
return [ 'cycle' => [ 'collections' => [ 'factories' => [ 'custom' => App\Cycle\Collection\CustomCollectionFactory::class, ], ], ], ];
With the short form, the package registers the factory by alias and lets Cycle infer the collection interface from
CustomCollectionFactory::getInterface(). If the factory class has constructor dependencies, register it as a container
service and use that service ID instead of the class name.
Use the extended definition only when you need to explicitly provide a base collection class or interface for
collection: CustomCollection::class matching:
return [ 'cycle' => [ 'collections' => [ 'factories' => [ 'custom' => [ 'factory' => App\Cycle\Collection\CustomCollectionFactory::class, 'interface' => App\Collection\BaseCollection::class, ], ], ], ], ];
The factory value in the extended definition accepts the same inputs as the short form:
a container service ID, a factory FQCN with a zero-arg constructor, or a direct factory instance.
For ManyToMany pivot entity access specifically, use a collection factory that supports Cycle pivoted collections.
Cycle's Doctrine collection factory provides that support, so install doctrine/collections, register doctrine,
and set the relation collection to doctrine.
Manual mapping key compatibility
Manual schema definitions are configured with cycle.schema.manual_mapping_schema_definitions.
Services
Provided by ConfigProvider:
dbal->Cycle\Database\DatabaseManagerCycle\Database\DatabaseProviderInterface-> alias ofdbalCycle\Database\DatabaseInterface-> default database resolved from the samedbalmanagerorm->Cycle\ORM\ORMInterface
Migration aliases provided only when cycle/migrations is installed:
migrator->Sirix\Cycle\Service\MigratorInterface
The ORM, commands, schema generation, and database factories all resolve dbal. Configure your
container to share that service so they receive the same DatabaseManager. Laminas ServiceManager
shares services by default; other containers and custom sharing settings may behave differently.
Use the provider to select any configured database:
use Cycle\Database\DatabaseProviderInterface; /** @var DatabaseProviderInterface $provider */ $provider = $container->get(DatabaseProviderInterface::class); $analytics = $provider->database('analytics');
Cycle\Database\DatabaseInterface resolves to the default database of that manager:
use Cycle\Database\DatabaseInterface; /** @var DatabaseInterface $db */ $db = $container->get(DatabaseInterface::class); $db->transaction(static function (DatabaseInterface $db): void { // atomic work; Cycle commits on success and rolls back on exception });
Transactions belong to the database's write driver. ORM operations participate in that transaction only when they use the same write connection.
Named database services with Laminas ServiceManager
When laminas/laminas-servicemanager is installed, NamedDatabaseAbstractFactory lets an
application expose named Cycle databases as explicit container services. List the service IDs
and the Cycle database names they map to in cycle.database_services:
use Cycle\Database\Config\SQLiteDriverConfig; return [ 'cycle' => [ 'database_services' => [ 'db.analytics' => 'analytics', 'db.archive' => 'archive', ], 'db-config' => [ 'default' => 'default', 'databases' => [ 'default' => ['connection' => 'primary'], 'analytics' => ['connection' => 'reporting'], 'archive' => ['connection' => 'archive'], ], 'connections' => [ 'primary' => new SQLiteDriverConfig(), 'reporting' => new SQLiteDriverConfig(), 'archive' => new SQLiteDriverConfig(), ], ], ], 'dependencies' => [ 'aliases' => [ // Replace with an interface declared by your application. App\Database\AnalyticsDatabaseInterface::class => 'db.analytics', ], ], ];
This example uses separate in-memory SQLite connections. Replace them with your application's
driver configurations. The map values are Cycle database names from db-config.databases, not
connection names.
$container->get('db.analytics') returns $container->get('dbal')->database('analytics') from the
same manager when dbal is shared. Only service IDs listed in cycle.database_services are
intercepted; unknown services are left to the container. For other PSR-11 containers, register
named databases with your own factory using explicit service IDs.
Transaction lifetime
Fixing the alias does not change connection lifetime. DatabaseManager caches databases and drivers.
If a long-running worker shares dbal between requests, the same database object and write driver
may survive between them. Prefer $db->transaction(static function (DatabaseInterface $db): void { /* work */ });
it commits on success and rolls back on exception. A manual begin() must be closed with an explicit
try/catch/finally on every path. Request-boundary cleanup belongs to the application or runner,
not to these factories.
Repository services
RepositoryFactory exposes a custom Cycle repository as a container service without injecting ORMInterface into application code.
Assign the repository class to an entity in the Cycle schema, register it with the factory, and alias your application interface to it:
use App\Entity\User; use App\Repository\UserRepository; use App\Repository\UserRepositoryInterface; use Cycle\Annotated\Annotation\Entity; use Sirix\Cycle\Factory\RepositoryFactory; #[Entity(repository: UserRepository::class)] final class User {} return [ 'dependencies' => [ 'factories' => [ UserRepository::class => RepositoryFactory::class, ], 'aliases' => [ UserRepositoryInterface::class => UserRepository::class, ], ], ];
The factory resolves the repository class from the compiled Cycle schema and asks Cycle for the corresponding entity role. It supports custom read repositories and repositories that add persist methods through EntityManager.
See the Repository Services guide for read and persist repository examples, manual schema configuration, string roles, aliases, and schema-cache refresh requirements.
CLI commands
Commands are registered only when symfony/console is installed.
cycle:schema:* commands:
cycle:schema:compile: compile schema and store compiled file.cycle:schema:sync: run sync pipeline; refresh compiled file only when cache is enabled.cycle:schema:migration:generate: generate migrations via schema pipeline; available only withcycle/migrationsandcycle/schema-migrations-generator.
Other commands:
cycle:cache:clear: remove compiled schema file.cycle:migration:runcycle:migration:rollbackcycle:migration:createcycle:seed:createcycle:seed:run
Migration/seed command availability:
- registered only when
cycle/migrationsis installed.
Create migration notes
cycle:migration:create supports --database (-b).
Generated filename includes database alias:
<timestamp>_0_<counter>_<database-alias>_<migration_name_in_snake_case>.php
CLI usage examples
With laminas-cli:
php vendor/bin/laminas cycle:schema:compile php vendor/bin/laminas cycle:schema:sync php vendor/bin/laminas cycle:migration:create CreateUsers --database default
With standalone Symfony Console (manual command wiring in your app):
php bin/console cycle:schema:compile
Performance note
Compiled schema is stored as plain PHP and loaded by require, which works well with OPcache and avoids PSR-6 serialization overhead in runtime hot paths.