├── 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 | 
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 | 
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 |
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 |
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 |