{"id":15026779,"url":"https://github.com/andanteproject/shared-query-builder","last_synced_at":"2025-09-02T10:33:22.268Z","repository":{"id":62485737,"uuid":"345816273","full_name":"andanteproject/shared-query-builder","owner":"andanteproject","description":"A Doctrine 2 Query Builder decorator that makes easier to build your query in shared contexts","archived":false,"fork":false,"pushed_at":"2025-03-18T11:17:02.000Z","size":92,"stargazers_count":5,"open_issues_count":0,"forks_count":2,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-18T12:23:34.566Z","etag":null,"topics":["doctrine","doctrine-orm","php","php-74","php7","query-builder"],"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/andanteproject.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":"2021-03-08T22:45:18.000Z","updated_at":"2025-03-18T11:24:22.000Z","dependencies_parsed_at":"2024-06-14T08:28:33.857Z","dependency_job_id":"4961608b-9a99-4a71-a869-07eec25df7c4","html_url":"https://github.com/andanteproject/shared-query-builder","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andanteproject%2Fshared-query-builder","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andanteproject%2Fshared-query-builder/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andanteproject%2Fshared-query-builder/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andanteproject%2Fshared-query-builder/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/andanteproject","download_url":"https://codeload.github.com/andanteproject/shared-query-builder/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248104446,"owners_count":21048339,"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":["doctrine","doctrine-orm","php","php-74","php7","query-builder"],"created_at":"2024-09-24T20:05:04.844Z","updated_at":"2025-04-09T20:21:41.864Z","avatar_url":"https://github.com/andanteproject.png","language":"PHP","readme":"![Andante Project Logo](https://github.com/andanteproject/shared-query-builder/blob/main/andanteproject-logo.png?raw=true)\n\n# Shared Query Builder\n\n#### Doctrine 2/3 [Query Builder](https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/query-builder.html) decorator - [AndanteProject](https://github.com/andanteproject)\n\n[![Latest Version](https://img.shields.io/github/release/andanteproject/shared-query-builder.svg)](https://github.com/andanteproject/shared-query-builder/releases)\n![Github actions](https://github.com/andanteproject/shared-query-builder/actions/workflows/workflow.yml/badge.svg?branch=main)\n![Php8](https://img.shields.io/badge/PHP-%208.x-informational?style=flat\u0026logo=php)\n![PhpStan](https://img.shields.io/badge/PHPStan-Level%208-syccess?style=flat\u0026logo=php)\n\nA Doctrine 2 [Query Builder](https://www.doctrine-project.org/projects/doctrine-orm/en/3.2/reference/query-builder.html)\ndecorator that makes easier to build your query in shared contexts.\n\n## Why do I need this?\n\nWhen your query business logic is big and complex you are probably going to split its building process to different\nplaces/classes.\n\nWithout `SharedQueryBuilder` there is no way to do that unless *guessing Entity aliases*  and messing up with *join\nstatements*.\n\nThis [query builder](https://www.doctrine-project.org/projects/doctrine-orm/en/3.2/reference/query-builder.html)\ndecorator addresses some problems you can find in a real world situation you usually solve with workarounds and business\nconventions.\n\n## Features\n\n- Ask [query builder](https://www.doctrine-project.org/projects/doctrine-orm/en/3.2/reference/query-builder.html)\n  which alias is used for an entity when you are outside its creation context;\n- **Lazy joins** to declare join statements to be performed only if related criteria are defined;\n- **Immutable** and **unique** query **parameters**;\n- Works like magic ✨.\n\n## Requirements\n\nDoctrine 2 and PHP 7.4.\n\n## Install\n\nVia [Composer](https://getcomposer.org/):\n\n```bash\n$ composer require andanteproject/shared-query-builder\n```\n\n## Set up\n\nAfter creating\nyour [query builder](https://www.doctrine-project.org/projects/doctrine-orm/en/3.2/reference/query-builder.html), wrap\nit inside our `SharedQueryBuilder`.\n\n```php\nuse Andante\\Doctrine\\ORM\\SharedQueryBuilder;\n\n// $qb instanceof Doctrine\\ORM\\QueryBuilder\n// $userRepository instanceof Doctrine\\ORM\\EntityRepository\n$qb = $userRepository-\u003ecreateQueryBuilder('u');\n// Let's wrap query builder inside our decorator.\n// We use $sqb as acronym of \"Shared Query Builder\"\n$sqb = SharedQueryBuilder::wrap($qb);\n```\n\nFrom now on, you can use `$sqb` exactly as you usually do\nwith [query builder](https://www.doctrine-project.org/projects/doctrine-orm/en/3.2/reference/query-builder.html) (every\nsingle method of `QueryBuilder` is available on `SharedQueryBuilder`), **but with some useful extra methods** 🤫.\n\nWhen you're done building your query, just **unwrap** your `SharedQueryBuilder`.\n\n```php\n// $sqb instanceof Andante\\Doctrine\\ORM\\SharedQueryBuilder\n// $qb instanceof Doctrine\\ORM\\QueryBuilder\n$qb = $sqb-\u003eunwrap();\n```\n\n#### Please note:\n\n- The only condition applied to build a `SharedQueryBuilder` is that no join statement has to be declared yet.\n- `SharedQueryBuilder` is *a decorator* of `QueryBuilder`, which means it is not an `instance of QueryBuilder` even if\n  it has all its methods (sadly, Doctrine has no QueryBuilder Interface 🥺).\n- `SharedQueryBuilder` do not allow you to join an Entity multiple times with different aliases.\n\n## Which additional methods do I have?\n\n### Entity methods\n\nYou can ask the `SharedQueryBuilder` if it has and entity in the `from` statement or some `join` statements.\n\n```php\nif($sqb-\u003ehasEntity(User::class)) // bool returned \n{ \n    // Apply some query criteria only if this query builder is handling the User entity\n}\n```\n\nYou can ask which is the alias of an Entity inside the query you're building (no matter if it is used in a `from`\nstatement or a `join` statement).\n\n```php\n$userAlias = $sqb-\u003egetAliasForEntity(User::class); // string 'u' returned \n```\n\nYou can use `withAlias` method to smoothly add a condition for that entity property:\n\n```php\nif($sqb-\u003ehasEntity(User::class)) // bool returned \n{ \n    $sqb\n        -\u003eandWhere(\n            $sqb-\u003eexpr()-\u003eeq(\n                $sqb-\u003ewithAlias(User::class, 'email'), // string 'u.email'\n                ':email_value'\n            )\n        )\n        -\u003esetParameter('email_value', 'user@email.com')\n    ;    \n} \n```\n\nGiven an alias, you can retrieve its entity class:\n\n```php\n$entityClass = $sqb-\u003egetEntityForAlias('u'); // string 'App\\Entity\\User' returned\n```\n\n`QueryBuilder::getAllAliases` is extended to have an optional `bool` argument `$includeLazy` (default:`false`) to\ninclude [lazy joins](#lazy-join) aliases.\n\n```php\n$allAliases = $sqb-\u003egetAllAliases(true);\n```\n\n### Lazy Join\n\nAll query builder `join` methods can be used as usually, but you can also use them with \"`lazy`\" prefix.\n\n```php\n// Common join methods\n$sqb-\u003ejoin(/* args */);\n$sqb-\u003einnerJoin(/* args */);\n$sqb-\u003eleftJoin(/* args */);\n\n// Lazy join methods\n$sqb-\u003elazyJoin(/* args */);\n$sqb-\u003elazyInnerJoin(/* args */);\n$sqb-\u003elazyLeftJoin(/* args */);\n\n// They works with all the ways you know you can perform joins in Doctrine\n// A: $sqb-\u003elazyJoin('u.address', 'a') \n// or B: $sqb-\u003elazyJoin('Address::class', 'a', Expr\\Join::WITH, $sqb-\u003eexpr()-\u003eeq('u.address','a')) \n```\n\nBy doing this, you are defining a `join` statement **without actually adding it** to your DQL query. It is going to be\nadded to your DQL query only when you add **another condition/dql part** which refers to it. Automagically ✨.\n\nBased on how much confused you are right now, you can check for [why you should need this](#why-do-i-need-this)\nor [some examples](#examples) to achieve your \"OMG\" revelation moment.\n\n## Examples\n\nLet's suppose we need to list `User` entities but we also have an **optional filter** to search an user by it's\naddress `Building` name.\n\nThere is no need to perform any join until we decide to use that filter. We can use **Lazy Join** to achieve this.\n\n```php\n$sqb = SharedQueryBuilder::wrap($userRepository-\u003ecreateQueryBuilder('u'));\n$sqb\n    -\u003elazyJoin('u.address', 'a')\n    -\u003elazyJoin('a.building', 'b')\n    //Let's add a WHERE condition that do not need our lazy joins \n    -\u003eandWhere(\n        $sqb-\u003eexpr()-\u003eeq('u.verifiedEmail', ':verified_email')\n    )\n    -\u003esetParameter('verified_email', true)\n;\n\n$users = $sqb-\u003egetQuery()-\u003egetResults();\n// DQL executed:\n//     SELECT u\n//     FROM App\\entity\\User\n//     WHERE u.verifiedEmail = true\n\n// BUT if we use the same Query Builder to filter by building.name:\n$buildingNameFilter = 'Building A';\n$sqb\n    -\u003eandWhere(\n        $sqb-\u003eexpr()-\u003eeq('b.name', ':name_value')\n    )\n    -\u003esetParameter('name_value', $buildingNameFilter)\n;\n$users = $sqb-\u003egetQuery()-\u003egetResults();\n// DQL executed:\n//     SELECT u\n//     FROM App\\entity\\User\n//       JOIN u.address a\n//       JOIN a.building b\n//     WHERE u.verifiedEmail = true\n//       AND b.name = 'Building A'\n```\n\nYou are probably thinking: **why don't we achieve the same result with the following, more common, way**? (keep in mind\nthat avoid to perform unecessary joins is still a requirement)\n\n```php\n// How you could achieve this without SharedQueryBuilder\n$buildingNameFilter = 'Building A';\n$qb = $userRepository-\u003ecreateQueryBuilder('u');\n$qb\n    -\u003eandWhere(\n        $qb-\u003eexpr()-\u003eeq('u.verifiedEmail', ':verified_email')\n    )\n    -\u003esetParameter('verified_email', true);\n    \nif(!empty($buildingNameFilter)){\n    $qb\n        -\u003elazyJoin('u.address', 'a')\n        -\u003elazyJoin('a.building', 'b')\n        -\u003eandWhere(\n            $qb-\u003eexpr()-\u003eeq('b.name', ':building_name_value')\n        )\n        -\u003esetParameter('building_name_value', $buildingNameFilter)\n    ;\n}\n\n$users = $qb-\u003egetQuery()-\u003egetResults(); // Same result as example shown before\n// But this has some down sides further explained\n```\n\nThe code above is perfectly fine if you build this whole query in the **same context**:\n\n- 👍 You are *aware* of the whole query building process;\n- 👍 You are *aware* of which entities are involved;\n- 👍 You are *aware* of which alias are defined for each entity.\n- 👍 You are *aware* of which query parameters are defined and their purpose.\n\nBut you have problems:\n\n- 👎 You are mixing query structure definition with optional filtering criteria.\n- 👎 Code is is quickly going to be an unreadable mess.\n\n### A real world case\n\nIf your query structure grows with lots of joins and filtering criteria, you are probably going to split all that\nbusiness logic in different classes.\n\nFor instance, in a backoffice Users list, you are probably going to define your *main query* to list entities in your\ncontroller and handle **optional filters** in some **other classes**.\n\n```php\n// UserController.php\nclass UserController extends Controller\n{\n    public function index(Request $request, UserRepository $userRepository) : Response\n    {\n        $qb = $userRepository-\u003ecreateQueryBuilder('u');\n        $qb\n            -\u003eandWhere(\n                $qb-\u003eexpr()-\u003eeq('u.verifiedEmail', ':verified_email')\n            )\n            -\u003esetParameter('verified_email', true);\n        \n        // Now Apply some optional filters from Request\n        // Let's suppose we have an \"applyFilters\" method which is giving QueryBuilder and Request\n        // to and array of classes responsable to take care of filtering query results.  \n        $this-\u003eapplyFilters($qb, $request);\n        \n        // Maybe have some pagination logic here too. Check KnpLabs/knp-components which is perfect for this.\n        \n        $users = $qb-\u003egetQuery()-\u003egetResults();\n        // Build our response with User entities list.\n    }\n}\n```\n\nFilter classes may look like this:\n\n```php\n// BuildingNameFilter.php\nclass BuildingNameFilter implements FilterInterface\n{\n    public function filter(QueryBuilder $qb, Request $request): void\n    {\n        $buildingNameFilter = $request-\u003equery-\u003eget('building-name');\n        if(!empty($buildingNameFilter)){\n            $qb\n                -\u003ejoin('u.address', 'a')\n                -\u003ejoin('a.building', 'b')\n                -\u003eandWhere(\n                    $qb-\u003eexpr()-\u003eeq('b.name', ':building_name_value')\n                )\n                -\u003esetParameter('building_name_value', $buildingNameFilter)\n            ;\n        }\n    }\n}\n```\n\n**We are committing some multiple sins here! 💀 The context is changed.**\n\n- 👎 You are *not aware* of the whole query building process. Is the given QueryBuilder even a query on User entity?;\n- 👎 You are *not aware* of which entities are involved. Which entities are already been joined?;\n- 👎 You are *not aware* of which aliases are defined for each entity. No way we are calling `u.address` by convention\n  🤨;\n- 👎 You are *aware* of what parameters have been defined (`$qb-\u003egetParameters()`), but you are *not aware* why they\n  have been defined, for which purpose and you can also *override* them changing elsewhere behavior;\n- 👎 Our job in this context is just to apply some filter. We *can* change the query by adding some join statements but\n  we *should avoid* that. What if another filter also need to perform those joins? Devastating. 😵\n\n#### This's why SharedQueryBuilder is going to save your ass in these situations\n\nLet's see how we can solve all these problems with `SharedQueryBuilder` (you can now guess why it is named like this).\n\nUsing `SharedQueryBuilder` you can:\n\n- 👍 Define **lazy join** to allow them to be performed only if they are needed;\n- 👍 Define some parameters **immutable** to be sure value is not going to be changed elsewhere;\n- 👍 You can **check if an entity is involved in a query** and then apply some business logic;\n- 👍 You can **ask the query builder** which *alias* is used for a specific entity so you are not going to guess aliases\n  or sharing them between classes using constants (I know you thought of that 🧐).\n\n```php\n// UserController.php\nuse Andante\\Doctrine\\ORM\\SharedQueryBuilder;\n\nclass UserController extends Controller\n{\n    public function index(Request $request, UserRepository $userRepository) : Response\n    {\n        $sqb = SharedQueryBuilder::wrap($userRepository-\u003ecreateQueryBuilder('u'));\n        $sqb\n            // Please note: Sure, you can mix \"normal\" join methods and \"lazy\" join methods\n            -\u003elazyJoin('u.address', 'a')\n            -\u003elazyJoin('a.building', 'b')\n            -\u003eandWhere($sqb-\u003eexpr()-\u003eeq('u.verifiedEmail', ':verified_email'))\n            -\u003esetImmutableParameter('verified_email', true);\n        \n        // Now Apply some optional filters from Request\n        // Let's suppose we have an \"applyFilters\" method which is giving QueryBuilder and Request\n        // to and array of classes responsable to take care of filtering query results.  \n        $this-\u003eapplyFilters($sqb, $request);\n        \n        // Maybe have some pagination logic here too.\n        // You probably need to unwrap the Query Builder now for this\n        $qb = $sqb-\u003eunwrap();\n        \n        $users = $qb-\u003egetQuery()-\u003egetResults();\n        // Build our response with User entities list.\n    }\n}\n```\n\nFilter classes will look like this:\n\n```php\n// BuildingNameFilter.php\nuse Andante\\Doctrine\\ORM\\SharedQueryBuilder;\n\nclass BuildingNameFilter implements FilterInterface\n{\n    public function filter(SharedQueryBuilder $sqb, Request $request): void\n    {\n        $buildingNameFilter = $request-\u003equery-\u003eget('building-name');\n        // Let's check if Query has a Building entity in from or join DQL parts 🙌\n        if($sqb-\u003ehasEntity(Building::class) \u0026\u0026 !empty($buildingNameFilter)){\n            $sqb\n                -\u003eandWhere(\n                    // We can ask Query builder for the \"Building\" alias instead of guessing it/retrieve somewhere else 💋\n                    $sqb-\u003eexpr()-\u003eeq($sqb-\u003ewithAlias(Building::class, 'name'), ':building_name_value')\n                    // You can also use $sqb-\u003egetAliasForEntity(Building::class) to discover alias is 'b';\n                )\n                -\u003esetImmutableParameter('building_name_value', $buildingNameFilter)\n            ;\n        }\n    }\n}\n```\n\n- 👍 No extra join statements executed when there is no need for them;\n- 👍 No way to change/override parameters value once defined;\n- 👍 We can discover if the Query Builder is handling an Entity and then apply our business logic;\n- 👍 We are not guessing entity aliases;\n- 👍 Our filter class is only responsible for filtering;\n- 👍 There can be multiple filter class handling different criteria on the same entity without having duplicated join\n  statements;\n\n#### Immutable Parameters\n\nShared query builder has **Immutable Parameters**. Once defined, they cannot be changed otherwise and *Exception* will\nbe raised.\n\n```php\n// $sqb instanceof Andante\\Doctrine\\ORM\\SharedQueryBuilder\n\n// set a common Query Builder parameter, as you are used to \n$sqb-\u003esetParameter('parameter_name', 'parameterValue');\n\n// set an immutable common Query Builder parameter. It cannot be changed otherwise an exception will be raised.\n$sqb-\u003esetImmutableParameter('immutable_parameter_name', 'parameterValue');\n\n// get a collection of all query parameters (commons + immutables!)\n$sqb-\u003egetParameters();\n\n// get a collection of all immutable query parameters (exclude commons)\n$sqb-\u003egetImmutableParameters();\n\n// Sets a parameter and return parameter name as string instead of $sqb.\n$sqb-\u003ewithParameter(':parameter_name', 'parameterValue');\n$sqb-\u003ewithImmutableParameter(':immutable_parameter_name', 'parameterValue');\n// This allows you to write something like this:\n$sqb-\u003eexpr()-\u003eeq('building.name', $sqb-\u003ewithParameter(':building_name_value', $buildingNameFilter));\n\n// The two following methods sets \"unique\" parameters. See \"Unique parameters\" doc section for more...\n$sqb-\u003ewithUniqueParameter(':parameter_name', 'parameterValue');\n$sqb-\u003ewithUniqueImmutableParameter(':parameter_name', 'parameterValue');\n```\n\n#### Set parameter and use it in expression at the same moment\n\nIf you are sure you are not going to use a parameter in multiple places inside your query, you can write the following\ncode 🙌\n\n```php\n$sqb\n    -\u003eandWhere(\n        $sqb-\u003eexpr()-\u003eeq(\n            $sqb-\u003ewithAlias(Building::class, 'name'), \n            ':building_name_value'\n        )\n    )\n    -\u003esetImmutableParameter('building_name_value', $buildingNameFilter)\n;\n```\n\nthis way 👇👇👇\n\n```php\n$sqb\n    -\u003eandWhere(\n        $sqb-\u003eexpr()-\u003eeq(\n            $sqb-\u003ewithAlias(Building::class, 'name'), \n            $sqb-\u003ewithImmutableParameter(':building_name_value', $buildingNameFilter) // return \":building_name_value\" but also sets immutable parameter\n        )\n    )\n;\n\n```\n\n#### Unique parameters\n\nBeside [immutable parameters](#immutable-parameters), you can also demand query builder the generation of a parameter\nname. Using the following methods, query builder will decorate names to avoid conflicts with already declared ones (\nwhich cannot even happen with immutable parameters).\n\n```php\n$sqb\n    -\u003eandWhere(\n        $sqb-\u003eexpr()-\u003eeq(\n           'building.name', \n            $sqb-\u003ewithUniqueParameter(':name', $buildingNameFilter) // return \":param_name_4b3403665fea6\" making sure parameter name is not already in use and sets parameter value.\n        )\n    )\n    -\u003eandWhere(\n        $sqb-\u003eexpr()-\u003egte(\n           'building.createdAt', \n            $sqb-\u003ewithUniqueImmutableParameter(':created_at', new \\DateTime('-5 days ago'))  // return \":param_created_at_5819f3ad1c0ce\" making sure parameter name is not already in use and sets immutable parameter value.\n        )\n    )\n    -\u003eandWhere(\n        $sqb-\u003eexpr()-\u003elte(\n           'building.createdAt',\n            $sqb-\u003ewithUniqueImmutableParameter(':created_at', new \\DateTime('today midnight'))  // return \":param_created_at_604a8362bf00c\" making sure parameter name is not already in use and sets immutable parameter value.\n        )\n    )\n;\n\n/* \n * Query Builder has now 3 parameters:\n *  - param_name_4b3403665fea6 (common)\n *  - param_created_at_5819f3ad1c0ce (immutable)\n *  - param_created_at_604a8362bf00c (immutable)\n */\n```\n\n### Conclusion\n\nThe world is a happier place 💁.\n\nGive us a ⭐️ if your world is now a happier place too! 💃🏻\n\nBuilt with love ❤️ by [AndanteProject](https://github.com/andanteproject) team.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandanteproject%2Fshared-query-builder","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandanteproject%2Fshared-query-builder","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandanteproject%2Fshared-query-builder/lists"}