├── LICENSE ├── README.md ├── buildquery ├── BuildQueryPlugin.php ├── models │ └── BuildQueryModel.php ├── services │ └── BuildQueryService.php └── variables │ └── BuildQueryVariable.php ├── craft-build-query-logo.png └── examples └── happylager └── services ├── _entry.html ├── _entry_original_with_count.html └── _entry_original_without_count.html /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tighten Co. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Craft Build Query logo](https://raw.githubusercontent.com/tightenco/craft-build-query/master/craft-build-query-logo.png) 2 | 3 | # Building Complex Queries in Craft CMS 4 | 5 | A plugin for Craft CMS, demonstrating how to build complex or optimized queries by modifying an **ElementCriteriaModel** using `buildElementsQuery()`. 6 | 7 | For background on what this demo plugin is all about, see the post [Craft CMS: Building Complex Queries by Extending the ElementCriteriaModel](http://blog.tighten.co/craft-cms-building-complex-queries-by-extending-the-elementcriteriamodel) on the [Tighten.co](http://tighten.co/) blog. 8 | 9 | This plugin can be used as a starting point for adding your own advanced query logic, allowing you to perform queries that aren't possible using Craft's built-in methods. Using this plugin as a basis, you can, for instance: 10 | 11 | * Add a `join` clause to a query, to search data from a third-party plugin 12 | * Perform a complex `join` involving data from several tables 13 | * Optimize a query in order to reduce the number of database queries performed 14 | * Group, order, and aggregate results at the database level, rather than relying on the `group` filter in your Twig template 15 | 16 | ### Installation 17 | 18 | Add the `buildquery` folder to your `craft/app/plugins` directory, then activate the BuildQuery plugin in the _Settings_ section of Craft's control panel. 19 | 20 | ### Use 21 | 22 | To begin your query, call the `buildQuery` variable from within a Twig template, and pass it an initial ElementCriteriaModel as `source`: 23 | 24 | ```twig 25 | craft.buildQuery.source(...) 26 | ``` 27 | 28 | From there, you can chain additional query methods that you store in `BuildQueryService`, and finally grab your results with `find`: 29 | 30 | ```twig 31 | craft.buildQuery.source(serviceEntries).countRelated(workEntries).find 32 | ``` 33 | 34 | Take a look at `yourOwnMethod()` in `services/BuildQueryService.php` for a good place to start building your own complex query logic. 35 | 36 | ___ 37 | 38 | ### Example 39 | Using Craft's HappyLager demo site as an example, suppose we want to show the number of *Work* entries that are related to each *Service* entry in the Services navigation bar: 40 | 41 | ![Work counts](https://cloud.githubusercontent.com/assets/357312/12723250/a475b4e6-c8d6-11e5-981b-e0a35e2166ff.png) 42 | 43 | 44 | The typical way to do this would be to add a `relatedTo` query inside the loop where we output each Service, and grab each `total`: 45 | 46 | ```twig 47 | {% for serviceEntry in craft.entries.section('services') %} 48 | 49 | {# Perform a `relatedTo` query for each element in `serviceEntry` #} 50 | {% set workCount = craft.entries.section('work').relatedTo(serviceEntry).total() %} 51 | 52 |
  • 53 | 54 | {{ serviceEntry.title }} · {{ workCount }} 55 | 56 |
  • 57 | 58 | {% endfor %} 59 | ``` 60 | 61 | The downside to this standard approach is that we are firing an additional database query for each Service. If we have only 6 services, this isn't a huge deal; but if we wanted to calculate totals for 50 elements, all those extra queries would start to add up fast. 62 | 63 | Using `buildElementsQuery()`, we can optimize this count by attaching the `relatedTo` criteria to our original query, and adding a `COUNT` statement to our query's `SELECT` clause. This gives us the same results, but requires only 1 additional query—regardless of how many elements we have (`n`)—rather than performing `n+1` queries. 64 | 65 | ```twig 66 | {# Get ElementCriteriaModels for Service and Work sections #} 67 | {% set serviceEntries = craft.entries.section('services') %} 68 | {% set workEntries = craft.entries.section('work') %} 69 | 70 | {% for serviceEntry in craft.buildQuery.source(serviceEntries).countRelated(workEntries).find %} 71 | 72 |
  • 73 | 74 | {{ serviceEntry.title }} · {{ serviceEntry.workCount }} 75 | 76 |
  • 77 | 78 | {% endfor %} 79 | ``` 80 | 81 | ### To see the plugin example in action: 82 | 83 | 1. Install [Craft's HappyLager demo site](https://github.com/pixelandtonic/HappyLager) 84 | 2. Add and activate this plugin (see [Installation](#installation) above) 85 | 2. Rename the existing template file `templates/services/_entry.html` to `_entry_original.html` for safekeeping 86 | 3. Replace it with the example template file from this plugin, located at `examples/happylager/services/_entry.html` 87 | 4. Visit the **How It's Made** section, and click one of the Section tiles on the page (e.g. **Design**) 88 | 89 | ___ 90 | 91 | ### Debugging 92 | 93 | The plugin includes a few methods that are helpful when building and debugging a complex query. These can be called from within a Twig template to dump details about your query. 94 | 95 | * To display details about the ElementCriteriaModel that your query is built on: 96 | `{% do craft.buildQuery.debugCriteria %}` 97 | * To dump the underlying SQL query that is built after the ElementCriteriaModel has been converted to a dbCommand object: 98 | `{% do craft.buildQuery.debugSql %}` 99 | * To show the results of your query in array form, before they get populated into **EntryModel**s: 100 | `{% do craft.buildQuery.debugResults %}` 101 | 102 | -------------------------------------------------------------------------------- /buildquery/BuildQueryPlugin.php: -------------------------------------------------------------------------------- 1 | setContent($value); 48 | $models[] = $model; 49 | } 50 | 51 | return $models; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /buildquery/services/BuildQueryService.php: -------------------------------------------------------------------------------- 1 | criteria = $criteria; 46 | 47 | return $this; 48 | } 49 | 50 | /** 51 | * Include a count of related elements in our query results. 52 | * 53 | * @param ElementCriteriaModel $relation 54 | * @return BuildQueryService 55 | */ 56 | public function countRelated(ElementCriteriaModel $relation) 57 | { 58 | $this->addRelationToElementCriteriaModel($relation); 59 | $this->extendElementCriteriaModel(); 60 | $this->addCountToQuery('sources1.targetId', 'workCount'); 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Perform the find after the query has been built and return 67 | * an array of the resulting EntryModels or BuildQueryModels. 68 | * 69 | * @return Array 70 | */ 71 | public function find() 72 | { 73 | if (! $this->dbCommand) { 74 | return $this->criteria->find(); 75 | } 76 | 77 | $result = $this->dbCommand->queryAll(); 78 | 79 | return BuildQueryModel::populateModels($result); 80 | } 81 | 82 | /** 83 | * Add a `relatedTo` parameter to our ElementCriteriaModel. 84 | * 85 | * Accepts an ElementCriteriaModel representing the 86 | * elements that we want to relate to our source. 87 | * 88 | * @param ElementCriteriaModel $relation 89 | * @return void 90 | */ 91 | protected function addRelationToElementCriteriaModel(ElementCriteriaModel $relation) 92 | { 93 | $this->criteria->relatedTo = ['sourceElement' => $relation]; 94 | } 95 | 96 | /** 97 | * Convert our ElementCriteriaModel into a dbCommand object, 98 | * which we can use to add further parameters to our query. 99 | * 100 | * @return void 101 | */ 102 | protected function extendElementCriteriaModel() 103 | { 104 | if (! $this->dbCommand) { 105 | $this->dbCommand = craft()->elements->buildElementsQuery($this->criteria); 106 | } 107 | } 108 | 109 | /** 110 | * Add COUNT to our query's SELECT clause, 111 | * optionally naming the resulting count with an alias. 112 | * 113 | * @param String $field The field from the SQL query that we want to count 114 | * @param string $alias Optional name for the resulting count 115 | * @return void 116 | */ 117 | protected function addCountToQuery($field, $alias = 'count') 118 | { 119 | if (! $this->dbCommand) { 120 | return; 121 | } 122 | 123 | $this->dbCommand->addSelect('count(' . $field . ') AS ' . $alias); 124 | } 125 | 126 | /* 127 | |-------------------------------------------------------------------------- 128 | | Add your own query methods here 129 | |-------------------------------------------------------------------------- 130 | | 131 | | Use this method as a scaffold for adding your own query directives. 132 | | From your template, it can be chained onto other query directives 133 | | after your query has been set up with `craft.buildQuery.source()...` 134 | | 135 | */ 136 | 137 | /** 138 | * A scaffold for your own query method 139 | * 140 | * @return BuildQueryService 141 | */ 142 | public function yourOwnMethod($parameter) 143 | { 144 | // First, make sure we have a dbCommand object to work with: 145 | $this->extendElementCriteriaModel(); 146 | 147 | // ----------------------------------------------- 148 | // Add your own query logic here, using any method 149 | // that is available to the `dbCommand` class. 150 | // 151 | // $this->dbCommand->... 152 | // ----------------------------------------------- 153 | 154 | // Finally, return this BuildQueryService object, 155 | // so we can chain query methods in a template, like: 156 | // `craft.buildQuery.source(an_element_criteria_model).yourOwnMethod(parameter).find()` 157 | return $this; 158 | } 159 | 160 | /* 161 | |-------------------------------------------------------------------------- 162 | | For debugging 163 | |-------------------------------------------------------------------------- 164 | | 165 | | These methods are helpful when building an extended query, 166 | | to take a look at the underlying ElementCriteriaModel 167 | | and prepared SQL statement. 168 | | 169 | */ 170 | 171 | /** 172 | * Dump details about the ElementCriteriaModel 173 | * that our query is built upon. 174 | */ 175 | public function showCriteria() 176 | { 177 | Craft::dd($this->criteria->getAttributes()); 178 | } 179 | 180 | /** 181 | * Dump the underlying SQL query that is built 182 | * after the ElementCriteriaModel has been 183 | * converted to a dbCommand object. 184 | */ 185 | public function showSql() 186 | { 187 | print_r(str_replace( 188 | array_keys($this->dbCommand->params), 189 | array_values($this->dbCommand->params), 190 | $this->dbCommand->getText() 191 | )); 192 | 193 | Craft::dd(''); 194 | } 195 | 196 | /** 197 | * Dump the results of our query in array form, 198 | * before the results get populated into EntryModels. 199 | */ 200 | public function showResults() 201 | { 202 | if (! $this->dbCommand) { 203 | Craft::dd($this->criteria->find()); 204 | } 205 | 206 | Craft::dd($this->dbCommand->queryAll()); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /buildquery/variables/BuildQueryVariable.php: -------------------------------------------------------------------------------- 1 | buildQuery->setSource($criteria); 22 | } 23 | 24 | /** 25 | * Dump details about the ElementCriteriaModel 26 | * that our query is built upon. 27 | * 28 | * In a template, call {% do craft.buildQuery.debugCriteria %} 29 | */ 30 | public function debugCriteria() 31 | { 32 | craft()->buildQuery->showCriteria(); 33 | } 34 | 35 | /** 36 | * Dump the underlying SQL query that is built 37 | * after the ElementCriteriaModel has been 38 | * converted to a dbCommand object. 39 | * 40 | * In a template, call {% do craft.buildQuery.debugSql %} 41 | */ 42 | public function debugSql() 43 | { 44 | craft()->buildQuery->showSql(); 45 | } 46 | 47 | /** 48 | * Dump the results of our query in array form, 49 | * before the results get populated into EntryModels. 50 | * 51 | * In a template, call {% do craft.buildQuery.debugResults %} 52 | */ 53 | public function debugResults() 54 | { 55 | craft()->buildQuery->showResults(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /craft-build-query-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tighten/craft-build-query/4507b194aa04ce2692b84d1bb74f31d4c603ff1a/craft-build-query-logo.png -------------------------------------------------------------------------------- /examples/happylager/services/_entry.html: -------------------------------------------------------------------------------- 1 | {# 2 | # Services entry template 3 | # ----------------------- 4 | # 5 | # This template gets loaded whenever a Services entry's URL is requested, 6 | # because the Services section's Template setting is set to "services/_entry". 7 | # 8 | # An `entry` variable will be automatically passed to this template, which will 9 | # be set to the requested Services entry. 10 | -#} 11 | 12 | {% extends "_layouts/site" %} 13 | {% set title = entry.title %} 14 | 15 | {% block main %} 16 | 43 | 44 |
    45 |
    46 | {% include "_includes/content_header" %} 47 | {% include "_includes/article_body" %} 48 |
    49 |
    50 | 51 |
    52 | {% for block in entry.serviceBody %} 53 |
    54 |
    55 |
    56 |

    {{ block.heading }}

    57 | {{ block.text }} 58 |
    59 |
    60 | {% set photo = block.image.first() %} 61 | {% if photo %} 62 | {{ photo.title }} 63 | {% endif %} 64 |
    65 |
    66 |
    67 | {% endfor %} 68 |
    69 | 70 | {# Load the most recent Work entry that involved this service #} 71 | {% set workEntry = craft.entries.section('work').relatedTo(entry).first() %} 72 | {% if workEntry %} 73 |
    74 | {% set image = workEntry.featuredImage.last() %} 75 | {% if image %} 76 |
    77 | {% endif %} 78 | 79 |
    80 | 88 |
    89 |
    90 |

    {{ workEntry.title }}

    91 |

    {{ workEntry.heading }}

    92 | {% if workEntry.subheading %} 93 |

    {{ workEntry.subheading }}

    94 | {% endif %} 95 |

    View More

    96 |
    97 |
    98 |
    99 |
    100 | {% endif %} 101 | 102 | {% endblock %} 103 | 104 | -------------------------------------------------------------------------------- /examples/happylager/services/_entry_original_with_count.html: -------------------------------------------------------------------------------- 1 | {# 2 | # Services entry template 3 | # ----------------------- 4 | # 5 | # This template gets loaded whenever a Services entry's URL is requested, 6 | # because the Services section's Template setting is set to "services/_entry". 7 | # 8 | # An `entry` variable will be automatically passed to this template, which will 9 | # be set to the requested Services entry. 10 | -#} 11 | 12 | {% extends "_layouts/site" %} 13 | {% set title = entry.title %} 14 | 15 | {% block main %} 16 | 42 | 43 |
    44 |
    45 | {% include "_includes/content_header" %} 46 | {% include "_includes/article_body" %} 47 |
    48 |
    49 | 50 |
    51 | {% for block in entry.serviceBody %} 52 |
    53 |
    54 |
    55 |

    {{ block.heading }}

    56 | {{ block.text }} 57 |
    58 |
    59 | {% set photo = block.image.first() %} 60 | {% if photo %} 61 | {{ photo.title }} 62 | {% endif %} 63 |
    64 |
    65 |
    66 | {% endfor %} 67 |
    68 | 69 | {# Load the most recent Work entry that involved this service #} 70 | {% set workEntry = craft.entries.section('work').relatedTo(entry).first() %} 71 | {% if workEntry %} 72 |
    73 | {% set image = workEntry.featuredImage.last() %} 74 | {% if image %} 75 |
    76 | {% endif %} 77 | 78 |
    79 | 87 |
    88 |
    89 |

    {{ workEntry.title }}

    90 |

    {{ workEntry.heading }}

    91 | {% if workEntry.subheading %} 92 |

    {{ workEntry.subheading }}

    93 | {% endif %} 94 |

    View More

    95 |
    96 |
    97 |
    98 |
    99 | {% endif %} 100 | 101 | {% endblock %} 102 | -------------------------------------------------------------------------------- /examples/happylager/services/_entry_original_without_count.html: -------------------------------------------------------------------------------- 1 | {# 2 | # Services entry template 3 | # ----------------------- 4 | # 5 | # This template gets loaded whenever a Services entry's URL is requested, 6 | # because the Services section's Template setting is set to "services/_entry". 7 | # 8 | # An `entry` variable will be automatically passed to this template, which will 9 | # be set to the requested Services entry. 10 | -#} 11 | 12 | {% extends "_layouts/site" %} 13 | {% set title = entry.title %} 14 | 15 | {% block main %} 16 | 39 | 40 |
    41 |
    42 | {% include "_includes/content_header" %} 43 | {% include "_includes/article_body" %} 44 |
    45 |
    46 | 47 |
    48 | {% for block in entry.serviceBody %} 49 |
    50 |
    51 |
    52 |

    {{ block.heading }}

    53 | {{ block.text }} 54 |
    55 |
    56 | {% set photo = block.image.first() %} 57 | {% if photo %} 58 | {{ photo.title }} 59 | {% endif %} 60 |
    61 |
    62 |
    63 | {% endfor %} 64 |
    65 | 66 | {# Load the most recent Work entry that involved this service #} 67 | {% set workEntry = craft.entries.section('work').relatedTo(entry).first() %} 68 | {% if workEntry %} 69 |
    70 | {% set image = workEntry.featuredImage.last() %} 71 | {% if image %} 72 |
    73 | {% endif %} 74 | 75 |
    76 | 84 |
    85 |
    86 |

    {{ workEntry.title }}

    87 |

    {{ workEntry.heading }}

    88 | {% if workEntry.subheading %} 89 |

    {{ workEntry.subheading }}

    90 | {% endif %} 91 |

    View More

    92 |
    93 |
    94 |
    95 |
    96 | {% endif %} 97 | 98 | {% endblock %} 99 | --------------------------------------------------------------------------------