{"id":19857962,"url":"https://github.com/boldare/xsolve-associate","last_synced_at":"2025-10-06T21:04:09.033Z","repository":{"id":57085027,"uuid":"108397643","full_name":"boldare/xsolve-associate","owner":"boldare","description":"This library allows to collect objects and values through associations and provides some entity fetching optimizations for Doctrine ORM to address N+1 queries problem.","archived":false,"fork":false,"pushed_at":"2023-01-19T13:51:49.000Z","size":42,"stargazers_count":5,"open_issues_count":3,"forks_count":3,"subscribers_count":16,"default_branch":"master","last_synced_at":"2025-06-21T22:46:03.622Z","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/boldare.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}},"created_at":"2017-10-26T10:35:19.000Z","updated_at":"2023-01-31T12:46:40.000Z","dependencies_parsed_at":"2023-02-11T02:45:46.284Z","dependency_job_id":null,"html_url":"https://github.com/boldare/xsolve-associate","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/boldare/xsolve-associate","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boldare%2Fxsolve-associate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boldare%2Fxsolve-associate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boldare%2Fxsolve-associate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boldare%2Fxsolve-associate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/boldare","download_url":"https://codeload.github.com/boldare/xsolve-associate/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boldare%2Fxsolve-associate/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264320949,"owners_count":23590561,"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-11-12T14:20:31.435Z","updated_at":"2025-10-06T21:04:03.985Z","avatar_url":"https://github.com/boldare.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Build Status](https://travis-ci.org/xsolve-pl/xsolve-associate.svg?branch=master)](https://travis-ci.org/xsolve-pl/xsolve-associate)\n[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/xsolve-pl/xsolve-associate/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/xsolve-pl/xsolve-associate/?branch=master)\n[![Latest Stable Version](https://poser.pugx.org/xsolve-pl/associate/v/stable)](https://packagist.org/packages/xsolve-pl/associate)\n[![Total Downloads](https://poser.pugx.org/xsolve-pl/associate/downloads)](https://packagist.org/packages/xsolve-pl/associate)\n[![Monthly Downloads](https://poser.pugx.org/xsolve-pl/associate/d/monthly)](https://packagist.org/packages/xsolve-pl/associate)\n[![License](https://poser.pugx.org/xsolve-pl/associate/license)](https://packagist.org/packages/xsolve-pl/associate)\n\nTable of contents\n=================\n\n  * [Introduction](#introduction)\n  * [License](#license)\n  * [Getting started](#getting-started)\n  * [Usage examples](#usage-examples)\n    * [Collecting associated objects and values](#collecting-associated-objects-and-values)\n    * [Efficiently loading associated entities and solving N+1 queries problem](#efficiently-loading-associated-entities-and-solving-N+1-queries-problem)\n    * [Deferring association traversal to load entities in bulk](#deferring-association-traversal-to-load-entities-in-bulk)\n\nIntroduction\n============\n\nThis library allows to collect objects and values through associations\nand provides some entity fetching optimizations for Doctrine ORM to\naddress N+1 queries problem.\n\nIt can play especially nicely with `Deferred` implementation\nfrom `webonyx/graphql-php` allowing to significantly reduce number\nof database queries.\n\nLicense\n=======\n\nThis bundle is under the MIT license. See the complete license in `LICENSE` file.\n\nGetting started\n===============\n\nInclude this bundle in your project using Composer as follows\n(assuming it is installed globally):\n\n```bash\n$ composer require xsolve-pl/associate\n```\n\nFor more information on Composer see its\n[Introduction](https://getcomposer.org/doc/00-intro.md).\n\nTo get the basic collector you may use the facade provided with the library:\n\n```php\n\u003c?php\n$facade = new \\Xsolve\\Associate\\Facade();\n$basicCollector = $facade-\u003egetBasicCollector();\n```\n\nIf you want to use collector dedicated for Doctrine ORM, provide appropriate\nentity manager when instantiating the facade and retrieve dedicated collector:\n\n```php\n\u003c?php\n$facade = new \\Xsolve\\Associate\\Facade($entityManager);\n$doctrineOrmCollector = $facade-\u003egetDoctrineOrmCollector();\n```\n\nYou can also compose your own collectors using building blocks provided\nby this library. It's also possible to replace the facade provided\nand replace it with some configuration for DI container your framework uses.\n\nThat's all - now you're ready to go!\n\nUsage examples\n==============\n\nCollecting associated objects and values\n----------------------------------------\n\nFirst functionality provided by this library it to allow to retrieve all objects\nthat can be reached via specified associations starting from some base objects.\n\nLet's assume we have following classes defined:\n\n```php\n\u003c?php\n\nclass Car\n{\n    /**\n     * @var Engine|null\n     */\n    protected $engine;\n\n    /**\n     * @param Engine $engine\n     */\n    public function __construct(Engine $engine = null)\n    {\n        $this-\u003eengine = $engine;\n    }\n\n    /**\n     * @return Engine|null\n     */\n    public function getEngine()\n    {\n        return $this-\u003eengine;\n    }\n}\n\nclass Engine\n{\n    /**\n     * @var Part[]\n     */\n    public $parts;\n\n    /**\n     * @param Part[] $parts\n     */\n    public function __construct(array $parts)\n    {\n        $this-\u003eparts = $parts;\n    }\n}\n\nclass Part\n{\n    /**\n     * @var string\n     */\n    protected $name;\n\n    /**\n     * @param string $name\n     */\n    public function __construct(string $name)\n    {\n        $this-\u003ename = $name;\n    }\n\n    /**\n     * @return string\n     */\n    public function getName(): string\n    {\n        return $this-\u003ename;\n    }\n\n    /**\n     * @return string[]\n     */\n    public function getNameAsWords(): array\n    {\n        return explode(' ', $this-\u003ename);\n    }\n\n    /**\n     * @return array\n     */\n    public function getNameStats(): array\n    {\n        return ['wordCount' =\u003e count($this-\u003egetNameAsWords())];\n    }\n}\n```\n\nNow let's assume we have some instances of `Car` class in `$cars` array\nas well as some associated objects:\n\n```php\n\u003c?php\n$cars = [\n    $sportCar = new Car(\n        $fastEngine = new Engine([\n            $valve = new Part('valve'),\n            $cylinder= new Part('cylinder'),\n        ])\n    ),\n    $sedan = new Car(\n        $turboEngine = new Engine([\n            $valve,\n            $sparkPlug = new Part('nano spark plug'),\n            $smartCylinder = new Part('smart cylinder'),\n        ])\n    ),\n    $suv = new Car(),\n];\n```\n\nNow we'd like to collect all `Engine` instances that `$cars` are associated with.\nIt's as easy as this:\n\n```php\n\u003c?php\n$engines = $basicCollector-\u003ecollect($cars, ['engine']);\n// $engines ~= [$fastEngine, $turboEngine]; - order is not guaranteed.\n```\n\n**Important!** Note that the order of `$engines` is not guaranteed.\nIt's so because `\\SplObjectStorage` is used internally to assert the uniqueness\nof collected objects.\n\nWe can go further with that and collect objects that are two associations\naway from `$cars` by doing:\n\n```php\n\u003c?php\n$parts = $basicCollector-\u003ecollect($cars, ['engine', 'parts']);\n// $parts ~= [$valve, $cylinder, $sparkPlug, $smartCylinder]; - order is not guaranteed.\n```\n\nNote that only `$valve` will be included only once as it will be detected\nthat the same object was associated view `$fastEngine` and `$turboEngine`.\n\nIt is also possible to collect scalar values but in this case uniqueness will not\nbe imposed on them:\n\n```php\n\u003c?php\n$names = $basicCollector-\u003ecollect($cars, ['engine', 'parts', 'name']);\n// $names ~= ['valve', 'cylinder', 'spark plug', 'smart cylinder']; - order is not guaranteed.\n```\n\nIf given association yields an array with sequential numeric indices\nstarting with `0` it is automatically assumed that it is a collection\nof objects or scalars (i.e. that association links given object to many\nobjects or scalars). Therefore it's possible to write:\n\n```php\n\u003c?php\n$words = $basicCollector-\u003ecollect($cars, ['engine', 'parts', 'nameAsWords']);\n// $words ~= [\n//     'valve', 'cylinder', 'nano', 'spark', 'plug', 'smart', 'cylinder'\n// ]; - order is not guaranteed.\n```\n\nThis time and `cylinder` is present twice as it is scalar value\nand uniqueness was not imposed.\n\nHowever if an array is associative we can go even deeper when collecting values:\n\n```php\n\u003c?php\n$wordCounts = $basicCollector-\u003ecollect($cars, ['engine', 'parts', 'nameStats', '[wordCount]']);\n// $wordCounts ~= [1, 1, 3, 2]; - order is not guaranteed.\n```\n\nInternally [`symfony/property-access`](https://packagist.org/packages/symfony/property-access)\nis used to follow associations so they may be accessible in different ways -\nfor instance as a public property or via a getter method.\nPlease consult\n[its documentation](https://symfony.com/doc/current/components/property_access.html)\nfor possible options.\n\nEfficiently loading associated entities and solving N+1 queries problem\n-----------------------------------------------------------------------\n\nLet's assume that we're building an e-commerce website using\n[doctrine/orm](https://packagist.org/packages/doctrine/orm)\nfor persistence. One of the things we can run into is N+1 queries problem\nwhich occurs when we fetch some entities from database and then attempt\nto traverse their associations via getters.\n\nFor example we can have some products. Each of them has some variants which in turn\nhave a property storing available inventory quantity. Now we would like to find out\nwhich products are available for sale and we already have `Product` instances loaded\nfrom database (e.g. after taking into account some filters that user applied).\nWe could use code like this:\n\n```php\n\u003c?php\n$availableProducts = array_filter(\n    $products,\n    function(Product $product) {\n        foreach ($product-\u003egetVariants() as $variant) {\n            if ($variant-\u003egetInventoryQuantity() \u003e 0) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n);\n```\n\nWhile this will work perfectly fine it will incur one `SELECT` query each time we call\n`getVariants` method on given `Variant` instance for the first time. Hence if we want\nto check availability for 100 products we would end up with 101 database queries executed.\n\nYou can find out more about this problem\nat [5 Doctrine ORM Performance Traps You Should Avoid](https://tideways.io/profiler/blog/5-doctrine-orm-performance-traps-you-should-avoid)\nwritten by [Benjamin Eberlei](https://github.com/beberlei) -\nsee section titled in section *Lazy-Loading and N+1 Queries*.\nFour ways to address this problem are pointed out there.\n\nEager loading (solution 3) can be the simplest way to go in some cases\nbut in many cases we will find it too rigid. It is possible that we don't want specific\nassociation to be loaded always but just in some cases.\n\nOther solutions are more flexible, like using dedicated DQL query (solution 1)\nor triggering eager loading of entities after collecting their identifiers (solution 2).\n\nThese solutions would however result in clunky code and they have to be adjusted\ndepending on whether given association is of *-to-one* or *-to-many* type\nand whether entities that are already initialized are on the inverse or the owning\nside of the association. Also some minor optimizations can be applied\nif some `\\Doctrine\\Common\\Persistence\\Proxy` instances\nor `\\Doctrine\\ORM\\PersistentCollection` instances are already initialized\nand hence can be skipped.\n\nThis library tries to do exactly what is proposed in solutions 1 and 2\nbut in a clean and encapsulated manner. Thanks to it loading associated entities\nis simple and can be applied easily. In the example above it would be only required\nto precede previously given code with:\n\n```php\n\u003c?php\n$facade = new \\Xsolve\\Associate\\Facade($entityManager);\n$doctrineOrmCollector = $facade-\u003egetDoctrineOrmCollector();\n$doctrineOrmCollector-\u003ecollect($products, ['variants']);\n```\n\nAfter executing this snippet all variants for given products will be loaded\nwith a single `SELECT` query and calling `getVariants` will not result\nin any additional queries.\n\nIf the number of products or associated entities is high they'll be split\nin chunks and associations for each chunk will be loaded separately. Chunk size is\nset by default to `1000` but you are free to alter it\nor set it to `null` to disable chunking.\n\nAlso property values can be collected this way. If each variant has a property containing\nits price and we would like to collect prices of all variants of all given products\nwe could execute following code:\n\n```php\n\u003c?php\n$facade = new \\Xsolve\\Associate\\Facade($entityManager);\n$doctrineOrmCollector = $facade-\u003egetDoctrineOrmCollector();\n$prices = $doctrineOrmCollector-\u003ecollect($products, ['variants', 'price']);\n```\n\nIt's as simple as that!\n\n**Important!** You won't be able to reduce the number of queries for one-to-one\nassociations starting from inverse side - Doctrine ORM loads them by default issuing\na separate `SELECT` for each entity. You may consider changing such association to\none-to-many (and use collector afterwards) or using embeddable if possible (in which case\nembedded entities will be loaded with the same query that loads entities that contain them).\n\nDeferring association traversal to load entities in bulk\n--------------------------------------------------------\n\nIf you're working on a project using Doctrine ORM and providing GraphQL API\nthen this library can play nicely with `Deferred` class provided by\n[webonyx/graphql-php](https://packagist.org/packages/webonyx/graphql-php).\nYou can read more about the general idea behind this approach at\n[Solving N+1 Problem](http://webonyx.github.io/graphql-php/data-fetching/#solving-n1-problem)\nsection of its documentation.\n\nLet's assume we need to implement `resolve` function that will return `Variant` instances for\n`Product` instance. Basic implementation could look as follows:\n\n```php\n\u003c?php\n$resolve = function(Product $product) {\n    return $product-\u003egetVariants();\n};\n```\n\nBut using this approach we would again end up with N+1 queries executed against our database.\nTo alleviate this problem and to load these objects efficiently we can use instance of\n`BufferedCollector` like this:\n\n```php\n\u003c?php\n$facade = new \\Xsolve\\Associate\\Facade($entityManager);\n$bufferedCollector = $facade-\u003egetBufferedDoctrineOrmCollector();\n\n$resolve = function(Product $product) use ($bufferedCollector) {\n    $bufferedCollectClosure = $bufferedCollector-\u003ecreateCollectClosure([$product], ['variants']);\n\n    return new \\GraphQL\\Deferred(function() use ($bufferedCollectClosure) {\n        return $bufferedCollectClosure();\n    });\n};\n```\n\nEt voilà! What `BufferedCollector` will do it will accumulate all collect jobs\nwhile query result is build width first. When GraphQL library attempts to resolve\n`Deferred` that was returned in our `resolve` function the collector will group all similar\njobs stored before (comparing base object class and association path) and will load\nall of them in a single batch, issuing only 1 `SELECT` query\n(or 1 query for chunk if the number of base entities is high as mentioned above).\nHence we will end up with 2 queries instead of 101.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fboldare%2Fxsolve-associate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fboldare%2Fxsolve-associate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fboldare%2Fxsolve-associate/lists"}