{"id":15753611,"url":"https://github.com/mcaskill/charcoal-model-collection","last_synced_at":"2025-06-25T03:02:51.755Z","repository":{"id":62526085,"uuid":"237288134","full_name":"mcaskill/charcoal-model-collection","owner":"mcaskill","description":"Advanced model collection loaders (repositories) for Charcoal","archived":false,"fork":false,"pushed_at":"2023-10-18T14:29:00.000Z","size":42,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-06-25T03:01:51.241Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mcaskill.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-01-30T19:23:46.000Z","updated_at":"2023-10-18T14:29:06.000Z","dependencies_parsed_at":"2024-10-04T07:41:33.303Z","dependency_job_id":"3475e23b-5cca-495a-9ad6-e7842af13501","html_url":"https://github.com/mcaskill/charcoal-model-collection","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":"locomotivemtl/charcoal-contrib-template","purl":"pkg:github/mcaskill/charcoal-model-collection","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mcaskill%2Fcharcoal-model-collection","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mcaskill%2Fcharcoal-model-collection/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mcaskill%2Fcharcoal-model-collection/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mcaskill%2Fcharcoal-model-collection/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mcaskill","download_url":"https://codeload.github.com/mcaskill/charcoal-model-collection/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mcaskill%2Fcharcoal-model-collection/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261795268,"owners_count":23210612,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-04T07:41:26.807Z","updated_at":"2025-06-25T03:02:51.720Z","avatar_url":"https://github.com/mcaskill.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"Charcoal Model Collections / Repositories\n=========================================\n\n[![License][badge-license]][charcoal-model-collection]\n[![Latest Stable Version][badge-version]][charcoal-model-collection]\n[![Build Status][badge-travis]][dev-travis]\n\nSupport package providing advanced model collections and collection loaders for [Charcoal][charcoal-core] projects.\n\n\n\n## Installation\n\n```shell\ncomposer require mcaskill/charcoal-model-collection\n```\n\nSee [`composer.json`](composer.json) for depenencides.\n\n\n\n## Collections\n\n### 1. `Charcoal\\Support\\Model\\Collection\\Collection`\n\nProvides methods to manipulate the collection or retrieve specific models.\n\n#### `filter()`\n\nFilter the collection of objects using the given callback.\n\n```php\n$collection = new Collection([ $a, $b, $c, $d, $e ]);\n// [\n//     1 =\u003e Model (active: 1), 2 =\u003e Model (active: 0),\n//     3 =\u003e Model (active: 1), 4 =\u003e Model (active: 1),\n//     5 =\u003e Model (active: 0)\n// ]\n\n$filtered = $collection-\u003efilter(function ($obj, $id) {\n    return ($obj['active'] === true);\n});\n// [ 1 =\u003e Model, 3 =\u003e Model, 4 =\u003e Model ]\n```\n\n#### `forPage()`\n\n\"Paginate\" the collection by slicing it into a smaller collection.\n\n```php\n$collection = new Collection([ $a, $b, $c, $d, $e, $f, $g, $h, $i, $j ]);\n// [ 1 =\u003e Model, 2 =\u003e Model, 3 =\u003e Model, 4 =\u003e Model, 5 =\u003e Model,… ]\n\n$chunk = $collection-\u003eforPage(2, 3);\n// [ 4 =\u003e Model, 5 =\u003e Model, 6 =\u003e Model ]\n```\n\n#### `only()`\n\nExtract the objects with the specified keys.\n\n```php\n$collection = new Collection([ $a, $b, $c, $d, $e ]);\n// [ 1 =\u003e Model, 2 =\u003e Model, 3 =\u003e Model, 4 =\u003e Model, 5 =\u003e Model ]\n\n$filtered = $collection-\u003eonly(2);\n// [ 2 =\u003e Model ]\n\n$filtered = $collection-\u003eonly([ 1, 3 ]);\n// [ 1 =\u003e Model, 3 =\u003e Model ]\n\n$filtered = $collection-\u003eonly(2, 4);\n// [ 2 =\u003e Model, 4 =\u003e Model ]\n```\n\n#### `pop()`\n\nRemove and return the last object from the collection.\n\n```php\n$collection = new Collection([ $a, $b, $c, $d, $e ]);\n// [ 1 =\u003e Model, 2 =\u003e Model, 3 =\u003e Model, 4 =\u003e Model, 5 =\u003e Model ]\n\n$collection-\u003epop();\n// Model (5)\n\n$collection-\u003etoArray();\n// [ 1 =\u003e Model, 2 =\u003e Model, 3 =\u003e Model, 4 =\u003e Model ]\n```\n\n#### `prepend()`\n\nAdd an object onto the beginning of the collection.\n\n```php\n$collection = new Collection([ $a, $b, $c, $d, $e ]);\n// [ 1 =\u003e Model, 2 =\u003e Model, 3 =\u003e Model, 4 =\u003e Model, 5 =\u003e Model ]\n\n$collection-\u003eprepend($o);\n// Model (15)\n\n$filtered-\u003etoArray();\n// [ 15 =\u003e Model, 1 =\u003e Model, 2 =\u003e Model, 3 =\u003e Model, 4 =\u003e Model, 5 =\u003e Model ]\n```\n\n#### `random()`\n\nRetrieve one or more random objects from the collection.\n\n```php\n$collection = new Collection([ $a, $b, $c, $d, $e ]);\n// [ 1 =\u003e Model, 2 =\u003e Model, 3 =\u003e Model, 4 =\u003e Model, 5 =\u003e Model ]\n\n$collection-\u003erandom();\n// Model (3)\n\n$collection-\u003erandom(2);\n// [ 1 =\u003e Model, 3 =\u003e Model ]\n```\n\n#### `reverse()`\n\nReverse the order of objects in the collection.\n\n```php\n$collection = new Collection([ $a, $b, $c, $d, $e ]);\n// [ 1 =\u003e Model, 2 =\u003e Model, 3 =\u003e Model, 4 =\u003e Model, 5 =\u003e Model ]\n\n$reversed = $collection-\u003ereverse();\n// [ 5 =\u003e Model, 4 =\u003e Model, 3 =\u003e Model, 2 =\u003e Model, 1 =\u003e Model ]\n```\n\n#### `shift()`\n\nRemove and return the first object from the collection.\n\n```php\n$collection = new Collection([ $a, $b, $c, $d, $e ]);\n// [ 1 =\u003e Model, 2 =\u003e Model, 3 =\u003e Model, 4 =\u003e Model, 5 =\u003e Model ]\n\n$collection-\u003eshift();\n// Model (1)\n\n$collection-\u003etoArray();\n// [ 2 =\u003e Model, 3 =\u003e Model, 4 =\u003e Model, 5 =\u003e Model ]\n```\n\n#### `slice()`\n\nExtract a slice of the collection.\n\n```php\n$collection = new Collection([ $a, $b, $c, $d, $e, $f, $g, $h, $i, $j ]);\n// [ 1 =\u003e Model, 2 =\u003e Model, 3 =\u003e Model, 4 =\u003e Model, 5 =\u003e Model,… ]\n\n$slice = $collection-\u003eslice(4);\n// [ 5 =\u003e Model, 6 =\u003e Model, 7 =\u003e Model, 8 =\u003e Model, 9 =\u003e Model, 10 =\u003e Model ]\n\n$slice = $collection-\u003eslice(4, 2);\n// [ 5 =\u003e Model, 6 =\u003e Model ]\n```\n\n#### `sortBy()`\n\nSort the collection by the given callback or object property.\n\n```php\n$collection = new Collection([ $a, $b, $c ]);\n// [ 1 =\u003e Model (position: 5), 2 =\u003e Model (position: 2), 3 =\u003e Model (position: 0) ]\n\n$sorted = $collection-\u003esortBy('position');\n// [ 3 =\u003e Model, 2 =\u003e Model, 1 =\u003e Model ]\n```\n\n#### `sortByDesc()`\n\nSort the collection in descending order using the given callback or object property.\n\n#### `take()`\n\nExtract a portion of the first or last objects from the collection.\n\n```php\n$collection = new Collection([ $a, $b, $c, $d, $e, $f ]);\n// [ 1 =\u003e Model, 2 =\u003e Model, 3 =\u003e Model, 4 =\u003e Model, 5 =\u003e Model, 6 =\u003e Model ]\n\n$chunk = $collection-\u003etake(3);\n// [ 1 =\u003e Model, 2 =\u003e Model, 3 =\u003e Model ]\n\n$chunk = $collection-\u003etake(-2);\n// [ 5 =\u003e Model, 6 =\u003e Model ]\n```\n\n#### `where()`\n\nFilter the collection of objects by the given key/value pair.\n\n```php\n$collection = new Collection([ $a, $b, $c, $d, $e ]);\n// [\n//     1 =\u003e Model (active: 1), 2 =\u003e Model (active: 0),\n//     3 =\u003e Model (active: 1), 4 =\u003e Model (active: 1),\n//     5 =\u003e Model (active: 0)\n// ]\n\n$filtered = $collection-\u003ewhere('active', true);\n// [ 1 =\u003e Model, 3 =\u003e Model, 4 =\u003e Model ]\n```\n\n#### `whereIn()`\n\nFilter the collection of objects by the given key/value pair.\n\n```php\n$collection = new Collection([ $a, $b, $c, $d, $e ]);\n// [\n//     1 =\u003e Model (name: \"Lorem\"), 2 =\u003e Model (name: \"Ipsum\"),\n//     3 =\u003e Model (name: \"Dolor\"), 4 =\u003e Model (name: \"Elit\"),\n//     5 =\u003e Model (name: \"Amet\")\n// ]\n\n$filtered = $collection-\u003ewhereIn('name', [ 'Amet', 'Dolor' ]);\n// [ 3 =\u003e Model, 5 =\u003e Model ]\n```\n\n\n\n## Repositories\n\n### 1. `Charcoal\\Support\\Model\\Repository\\CollectionLoaderIterator`\n\nProvides improved counting of found rows (via `SQL_CALC_FOUND_ROWS`), supports PHP Generators via \"cursor\" methods, and supports chaining the loader directly into an iterator construct or appending additional criteria.\n\n\n#### 1.1. Lazy Collections\n\nThe `CollectionLoaderIterator` leverages PHP's [generators][php-syntax-generators] to allow you to work with large collections while keeping memory usage low.\n\nWhen using the traditional `load` methods, all models must be loaded into memory at the same time.\n\n```php\n$repository = (new CollectionLoaderIterator)-\u003esetModel(Post::class);\n\n$repository-\u003eaddFilter('active', true)-\u003eaddFilter('published \u003c= NOW()');\n\n$posts = $repository-\u003eload();\n// query: SELECT …\n// array: Post, Post, Post,…\n\nforeach ($posts as $post) {\n    echo $post['title'];\n}\n```\n\nHowever, the `cursor` methods return `Generator` objects instead. This allows you to keep one model loaded in memory at a time:\n\n```php\n$repository = (new CollectionLoaderIterator)-\u003esetModel(Post::class);\n\n$repository-\u003eaddFilter('active', true)-\u003eaddFilter('published \u003c= NOW()');\n\n$posts = $repository-\u003ecursor();\n// Generator\n\nforeach ($posts as $post) { // query: SELECT …\n    // Post\n    echo $post['title'];\n}\n```\n\n\n#### 1.2. `IteratorAggregate`\n\nThe `CollectionLoaderIterator` implements [`IteratorAggregate`][php-class-iterator-aggregate] which allows the repository to be used in a `foreach` construct without the need to explicitly call a query method.\n\n```php\nclass Post extends AbstractModel\n{\n    /**\n     * @return Comment[]|CollectionLoaderIterator\n     */\n    public function getComments() : iterable\n    {\n        $comments = (new CollectionLoaderIterator)-\u003esetModel(Comment::class);\n\n        $byPost = [\n            'property' =\u003e 'post_id',\n            'value'    =\u003e $this['id'],\n        ];\n\n        return $comments-\u003eaddFilter($byPost);\n    }\n}\n```\n\nInternally, the `IteratorAggregate::getIterator()` method calls the `CollectionLoaderIterator::cursor()` method which in turn returns a [`Generator`][php-class-generator] object.\n\n```php\n$post = $factory-\u003ecreate(Post::class)-\u003eload(1);\n\nforeach ($post['comments'] as $comment) { // query: SELECT …\n    // Comment\n}\n```\n\nFurthermore, you can continue to chain constraints onto the repository:\n\n```php\n$post = $factory-\u003ecreate(Post::class)-\u003eload(1);\n\n$comments = $post['comments']-\u003eaddFilter('approved', true);\n// CollectionLoaderIterator\n\nforeach ($comments as $comment) { // query: SELECT …\n    // Comment\n}\n```\n\n\n#### 1.3. `SQL_CALC_FOUND_ROWS`\n\nIf the [`SQL_CALC_FOUND_ROWS`][mysql-function-found-rows] option is included in a `SELECT` statement, the `FOUND_ROWS()` function will be invoked afterwards to retrieve the number of objects the statement would have returned without the `LIMIT`.\n\nUsing the query builder interface, the generated statement will include `SQL_CALC_FOUND_ROWS` option unless the query is targeting a single object.\n\n```php\n$repository = (new CollectionLoaderIterator)-\u003esetModel(Post::class);\n\n$repository-\u003eaddFilter('active', true)\n           -\u003eaddFilter('published \u003c= NOW()')\n           -\u003esetNumPerPage(10)\n           -\u003esetPage(3);\n\n// Automatically find total count from query builder\n$posts = $repository-\u003eload();\n// query: SELECT SQL_CALC_FOUND_ROWS * FROM `charcoal_users` WHERE ((`active` = '1') AND (`published` \u003c= NOW())) LIMIT 30, 10;\n// query: SELECT FOUND_ROWS();\n$total = $repository-\u003efoundObjs();\n// int: 38\n\n// Automatically find total count from query\n$users = $repository-\u003ereset()-\u003eloadFromQuery('SELECT SQL_CALC_FOUND_ROWS * … LIMIT 0, 20');\n// query: SELECT SQL_CALC_FOUND_ROWS * … LIMIT 0, 20;\n// query: SELECT FOUND_ROWS();\n$total = $repository-\u003efoundObjs();\n// int: 38\n\n// Automatically find total count from query\n$users = $repository-\u003ereset()-\u003eloadFromQuery('SELECT * … LIMIT 0, 20');\n// query: SELECT * … LIMIT 0, 20;\n$total = $repository-\u003efoundObjs();\n// LogicException: Can not count found objects for the last query\n```\n\n### 2. `Charcoal\\Support\\Model\\Repository\\ModelCollectionLoader`\n\nProvides support for cloning, preventing model swapping, and sharing the same [data source][charcoal-source-interface].\n\n#### 2.1. Model Protection\n\nOnce a model is assigned to the `ModelCollectionLoader`, any attempts to replace it will result in a thrown exception:\n\n```php\n$repository = (new ModelCollectionLoader)-\u003esetModel(Post::class);\n\n// …\n\n$repository-\u003esetModel(Comment::class);\n// RuntimeException: A model is already assigned to this collection loader: \\App\\Model\\Post\n```\n\nOn its own, this feature is not very practical but in concert with the `ScopedCollectionLoader` this becomes an important safety measure.\n\n\n#### 2.2. Collection Loader Cloning\n\nWhen cloning the `ModelCollectionLoader` via the `clone` keyword or the `cloneWith()` method, the model protection mechanism will be unlocked until a new object type is assigned or until the `source()` method is called.\n\n```php\n$postsLoader    = (new ModelCollectionLoader)-\u003esetModel(Post::class);\n$commentsLoader = (clone $postsLoader)-\u003esetModel(Comment::class);\n```\n\n```php\n$postsLoader    = (new ModelCollectionLoader)-\u003esetModel(Post::class);\n$commentsLoader = $postsLoader-\u003ecloneWith(Comment::class);\n$tagsLoader     = $postsLoader-\u003ecloneWith([\n    'model'      =\u003e Tag::class,\n    'collection' =\u003e 'array',\n]);\n```\n\n\n#### 2.3. Source Sharing\n\nA Charcoal Model is based on the ActiveRecord implementation for working with data sources; which is to say a Model allows you to interact with data in your database. This interaction is facilitated by a \"Data Source\" interface, like the `DatabaseSource` class. Each instance of a Model will usually create its own instance of a Data Source object; in other words, you end up always working with two objects per Model (the Model and the Data Source).\n\nTo reduce the number of objects in a request's lifecycle, its a good practice to assign a single instance of a Data Source to all Models. When the `ModelCollectionLoader` creates a new instance of the Model being queried, it will assign the _prototype_ Model's Data Source object (the one that is queried upon by the repository).\n\n```php\n$posts = (new BaseCollectionLoader)-\u003esetModel(Post::class)-\u003eload();\n// array: Post, Post, Post,…\n\n($posts[0]-\u003esource() === $posts[2]-\u003esource())\n// bool: false\n\n$posts = (new ModelCollectionLoader)-\u003esetModel(Post::class)-\u003eload();\n// array: Post, Post, Post,…\n\n($posts[0]-\u003esource() === $posts[2]-\u003esource())\n// bool: true\n```\n\n\n### 3. `Charcoal\\Support\\Model\\Repository\\ScopedCollectionLoader`\n\nProvides support for default filters, orders, and pagination, which are automatically applied upon the loader's creation and after every reset.\n\n```php\n$repository = new ScopedCollectionLoader([\n    'logger'          =\u003e $container['logger'],\n    'factory'         =\u003e $container['model/factory'],\n    'model'           =\u003e Post::class,\n    'default_filters' =\u003e [\n        [\n            'property' =\u003e 'active',\n            'value'    =\u003e true,\n        ],\n        [\n            'property' =\u003e 'publish_date',\n            'operator' =\u003e 'IS NOT NULL',\n        ],\n    ],\n    'default_orders'  =\u003e [\n        [\n            'property'  =\u003e 'publish_date',\n            'direction' =\u003e 'desc',\n        ],\n    ],\n    'default_pagination' =\u003e [\n        'num_per_page' =\u003e 20,\n    ],\n]);\n\n$posts = $repository-\u003eaddFilter('publish_date \u003c= NOW()')-\u003eload();\n// query: SELECT SQL_CALC_FOUND_ROWS * FROM `posts` WHERE ((`active` = '1') AND (`publish_date` IS NOT NULL) AND (`published` \u003c= NOW())) ORDER BY `publish_date` DESC LIMIT 20;\n\n$repository-\u003ereset()-\u003eload();\n// query: SELECT SQL_CALC_FOUND_ROWS * FROM `posts` WHERE ((`active` = '1') AND (`publish_date` IS NOT NULL)) ORDER BY `publish_date` DESC LIMIT 20;\n```\n\nIf you would like to disable the default criteria on a repository, you may use the `withoutDefaults` method. The method accepts a callback to interact with collection loader if, for example, you only wish to apply default orders:\n\n```php\n$repository = new ScopedCollectionLoader([…]);\n\n$posts = $repository-\u003ewithoutDefaults(function () {\n    $this-\u003eapplyDefaultOrders();\n    $this-\u003eapplyDefaultPagination();\n})-\u003eload();\n// query: SELECT SQL_CALC_FOUND_ROWS * FROM `posts` ORDER BY `publish_date` DESC LIMIT 20;\n```\n\n\n### 4. `Charcoal\\Support\\Model\\Repository\\CachedCollectionLoader`\n\nProvides support for storing the data of loaded models in a [cache pool][charcoal-cache], similar to the [`\\Charcoal\\Model\\Service\\ModelLoader`][charcoal-model-loader] and using the same cache key for  interoperability.\n\n```php\n$repository = new CachedCollectionLoader([\n    'cache'   =\u003e $container['cache'],\n    'logger'  =\u003e $container['logger'],\n    'factory' =\u003e $container['model/factory'],\n    'model'   =\u003e Post::class,\n]);\n```\n\nIf you would like to disable the caching process on a repository, you may use the `withoutCache` method. The method accepts a callback to interact with collection loader:\n\n```php\n$repository = new CachedCollectionLoader([…]);\n\n$posts = $repository-\u003ewithoutCache()-\u003ecursor();\n// Generator\n```\n\n\n\n## License\n\n-   _Charcoal Model Collections and Repositories_ component is licensed under the MIT license. See [LICENSE](LICENSE) for details.\n-   _Charcoal_ framework is licensed under the MIT license. See [LICENSE][license-charcoal] for details.\n\n\n\n[charcoal-model-collection]:    https://packagist.org/packages/mcaskill/charcoal-model-collection\n[charcoal-cache]:               https://packagist.org/packages/locomotivemtl/charcoal-cache\n[charcoal-core]:                https://packagist.org/packages/locomotivemtl/charcoal-core\n[charcoal-model-loader]:        https://github.com/locomotivemtl/charcoal-core/blob/master/src/Charcoal/Model/Service/ModelLoader.php\n[charcoal-source-interface]:    https://github.com/locomotivemtl/charcoal-core/blob/master/src/Charcoal/Source/SourceInterface.php\n[license-charcoal]:             https://github.com/locomotivemtl/charcoal-core/blob/master/LICENSE\n[mysql-function-found-rows]:    https://dev.mysql.com/doc/refman/5.7/en/information-functions.html#function_found-rows\n[php-syntax-generators]:        https://www.php.net/manual/en/language.generators.overview.php\n[php-class-generator]:          https://php.net/class.Generator\n[php-class-iterator-aggregate]: https://php.net/class.IteratorAggregate\n\n[dev-travis]:         https://travis-ci.org/mcaskill/charcoal-model-collection\n[badge-license]:      https://img.shields.io/packagist/l/mcaskill/charcoal-model-collection.svg?style=flat-square\n[badge-version]:      https://img.shields.io/packagist/v/mcaskill/charcoal-model-collection.svg?style=flat-square\n[badge-travis]:       https://img.shields.io/travis/mcaskill/charcoal-model-collection.svg?style=flat-square\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmcaskill%2Fcharcoal-model-collection","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmcaskill%2Fcharcoal-model-collection","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmcaskill%2Fcharcoal-model-collection/lists"}