Cycle ORM

repository·2.x·Indexed 23 days ago

https://github.com/cycle/orm

A high-performance PHP DataMapper and modeling engine designed for traditional request-response cycles and long-running daemonized applications like RoadRunner. It supports various relation types, POPOs, and multiple database systems including MySQL, MariaDB, PostgreSQL, SQL Server, and SQLite. Key features include a powerful query builder with eager and lazy loading, an immutable service core, and a disposable Unit of Work (UoW).

Tokens
5.7K
Snippets
4
Records
29
Agent score
79%

What's inside cycle-orm

  1. Overview of Cycle ORM

    2.x

    Cycle is a PHP DataMapper, ORM, and Data Modelling engine. It is specifically designed to work safely in both classic PHP environments and long-running, daemonized PHP applications (such as those powered by RoadRunner).

    Key capabilities include:

    • Data Modeling: Supports various relation types (has-one, has-many, many-through-many, polymorphic), embedded entities, and single table inheritance.
    • Object Support: Works with Plain Old PHP Objects (POPOs), Active Record, custom objects, or even classless entities.
    • Querying: Provides a powerful query builder with multiple fetch strategies, including eager and lazy loading.
    • Architecture: Designed for long-running processes using an immutable service core and a disposable Unit of Work (UoW).
    • Database Support: Compatible with MySQL, MariaDB, PostgreSQL, SQL Server, and SQLite.
    • Extensibility: Supports custom column types, custom mappers, relations, and persist strategies. It is compatible with PHP8 attributes and Doctrine Annotations.
  2. Use PivotedCollection to handle many-to-many relationship data

    2.x

    A PivotedCollection is a specialized ArrayCollection designed to hold entities alongside their associated pivot (junction table) data. This is particularly useful in many-to-many relationships where the relationship itself contains additional attributes (e.g., a User belongs to a Role via a Membership pivot entity containing granted_at).

    Important Note: The pivot context is tied to the collection instance. If the collection is partitioned or filtered, the association between the entity and its pivot data may be lost unless the new collection instance explicitly carries over the pivotContext.

  3. Load related data with load() and with()

    2.x

    Cycle ORM provides two ways to handle relations:

    1. load() (Eager Loading): Populates entity relations by executing additional queries (or a single JOIN if configured). This is used to actually hydrate the related objects into your entities.

      • Use LoadOptions (like HasManyLoadOptions) to configure how relations are loaded (e.g., custom where clauses, orderBy, or forcing a SingleQuery JOIN).
      • You can use the using keyword in load() to reuse a table already joined via with().
    2. with() (Query Joining): Joins related tables into the primary SQL query so you can filter or sort the root entities based on relation columns. Note: with() does not hydrate the relation data into the entities; it only makes the columns available for the query logic.

  4. Fetch and pre-load related entities with Cycle ORM

    2.x

    You can use the Repository to select entities with specific criteria and pre-load related collections. Pre-loading can be configured to use a single query (via LEFT JOIN) or separate queries. This example demonstrates fetching active users and pre-loading their paid orders, sorted by creation time.

    To persist changes, use the EntityManager to persist() the fetched entities and then call run() to execute the unit of work.

    // load all active users and pre-load their paid orders sorted from newest to olders
    // the pre-load will be complete using LEFT JOIN
    $users = $orm->getRepository(User::class)
        ->select()
        ->where('active', true)
        ->load('orders', [
            'method' => Select::SINGLE_QUERY,
            'load'   => function($q) {
                $q->where('paid', true)->orderBy('timeCreated', 'DESC');
            }
        ])
        ->fetchAll();
    
    $em = new EntityManager($orm);
    
    foreach($users as $user) {
        $em->persist($user);
    }
    
    $em->run();
  5. Configure JoinableLoadOptions for relation loading

    2.x

    When using Cycle\ORM\Select::load(), you can pass an instance of JoinableLoadOptions to control how related data is fetched. This class allows you to specify the loading method (JOIN vs separate query), apply custom scopes, set table aliases, or reuse existing joins to optimize performance.

    Key Configuration Properties

    • method: Determines the SQL strategy for loading the relation.
      • LoadMethod::SingleQuery: Performs an INLOAD (joins the relation into the parent query).
      • LoadMethod::OuterQuery: Performs a POSTLOAD (executes a separate SELECT query).
      • JoinMethod::InnerJoin: Performs an INNER JOIN (useful for filtering, but does not eager-load the data).
      • JoinMethod::LeftJoin: Performs a LEFT JOIN.
      • null: Uses the default loader configuration for that specific relation.
    • scope: Controls the query scope applied to the relation.
      • true: Uses the default scope defined on the relation source.
      • false: Disables all scopes (e.g., to include soft-deleted rows).
      • ScopeInterface: An instance of a custom scope.
    • as: A custom table alias for the relation in the generated SQL (e.g., 'rel').
    • using: Reuses an alias defined by another relation (typically from a previous with() call) instead of generating a new JOIN or query. This prevents duplicate JOINs.
    • table: Overrides the default table name used for loading the related entities.
    • minify: A boolean (default true) that, when true, minifies loader column aliases in the SQL output. Use with caution as disabling this may cause column name conflicts.
  6. Available Cycle ORM Extensions

    2.x

    Cycle ORM provides a variety of specialized extensions to enhance its functionality. These include components for Active Record patterns, schema building, migrations, entity behaviors, and more.

    Key extensions include:

    • cycle/active-record: Implements the Active Record pattern.
    • cycle/schema-builder, cycle/schema-renderer, cycle/schema-provider: Tools for managing database schemas.
    • cycle/annotated: Support for using annotations in entity definitions.
    • cycle/migrations: Database migration management.
    • cycle/entity-behavior and cycle/entity-behavior-uuid: Tools for adding logic and UUID support to entities.
    • cycle/database: Core database abstraction.
    • cycle/schema-migrations-generator: Generates migrations based on schema changes.
    • cycle/orm-promise-mapper: Mapping for promises.
  7. Override the source table with from()

    2.x

    The from() method allows you to override the default table defined in the entity schema. This is useful for reading from archive tables or partitions while keeping the same entity mapping, column aliases, and relations intact.

    // Read users from an archive table instead of the default one
    $select->from('user_archive')->where('id', 1)->fetchOne();
    
    // Combine with relations
    $select->from('user_archive')
        ->load('comments')
        ->orderBy('id')
        ->fetchAll();
  8. Access the underlying Select object from a Repository

    2.x
    To perform more complex query construction that is not covered by the high-level Repository methods, you can access the underlying Select object using the select() method. This method returns a clone of the internal selector, ensuring that the original repository remains immutable.
  9. Implement StoreCommandInterface for custom storage commands

    2.x

    If you are developing custom CLI commands that interact with the database storage, implement StoreCommandInterface. This interface extends CommandInterface and provides methods to manage data presence checks and data registration.

    Key capabilities include:

    • hasData(): Returns a boolean indicating if the command has data to process.
    • registerAppendix(string $key, mixed $value): Registers an optional value. If this value is present, the command will not execute if the primary data is empty. When executed, this appendix data is transferred to the entity state.
    • registerColumn(string $key, mixed $value): Adds raw data to be stored in a specific column.
  10. Filter queries with where() and logical operators

    2.x

    The where() method allows for complex filtering using several syntaxes:

    • Simple equality/comparison: where('column', 'value') or where('column', '>', 10).
    • BETWEEN: where('column', 'between', from, to).
    • Array syntax: where(['id' => 2]) or where(['id' => ['>' => 0, '<' => 3]]).
    • Logical grouping: Use @or or @AND keys in an array, or use closures for nested logic.
    • Raw SQL: Use Cycle\Database\Injection\Fragment for fragments with parameters or Cycle\Database\Injection\Expression for column-to-column comparisons.
    • JSON filtering: Use whereJson(), whereJsonContains(), whereJsonContainsKey(), etc., to query JSON fields.
    // Operator comparison
    $select->where('level', '>=', 10);
    
    // BETWEEN
    $select->where('comments.id', 'between', 1, 4);
    
    // Array syntax with operators
    $select->where(['comments.id' => ['between' => [1, 4]]]);
    
    // Logical grouping with @or
    $select->where([
        "@or" => [
            ['comments.message' => 'msg 1'],
            ['comments.message' => 'msg 3'],
        ],
    ]);
    
    // Nested logic with closures
    $select->where(function (\Cycle\ORM\Select\QueryBuilder $q): void {
        $q->where('comments.message', 'msg 3')
          ->orWhere(function (\Cycle\ORM\Select\QueryBuilder $q): void {
              $q->where('id', 1);
          });
    });
    
    // JSON path filtering
    $select->whereJson('settings->theme', 'dark');
  11. Use ScopeCarrierInterface methods to manage command scope

    2.x

    When working with commands that implement ScopeCarrierInterface, use the following methods to manage the lifecycle of scope data:

    • waitScope(string ...$keys): Instructs the command to wait for specific scope keys. The command must not be considered ready until these values are provided.
    • setScope(string $key, mixed $value): Provides a value for a specific scope key. This action should also remove the key from the command's internal wait-list.
    • getScope(): Retrieves an array of all currently set scope values.
    • getAffectedRows(): Returns the number of rows affected by the command execution.

    Note: Calling getAffectedRows() before the command has been executed will throw a Cycle\ORM\Exception\CommandException.

  12. Stream large datasets with cursor()

    2.x

    For processing massive datasets without exhausting memory, use cursor(). It returns a generator that yields hydrated entities one by one.

    • Chunking: The $chunkSize parameter controls how many distinct parent entities are processed before the ORM flushes the parser node. This is based on parent boundaries, not raw row counts.
    • Requirements: Requires a driver implementing \Cycle\Database\Driver\CursorInterface (e.g., Postgres, SQLite, SQL Server) and an active transaction.
    • Ordering: The ORM automatically appends the root primary key to the ORDER BY clause to ensure rows for the same parent arrive contiguously, which is critical for correct relation hydration during streaming.