As we learned in the first post about pdoTools, the main benefit of pdoTools is speed. In this second post we'll look at how pdoTools can be faster, and some more cool features that the library provides.
I developed the first version of pdoTools when working on Tickets. It was supposed to be a ticketing system, but when it was complete it was more like a simple blog system with awesome AJAX commenting. Tools like getResources were not fast enough, so I developed by own library.
The speed in pdoTools come from two ideas.
- The database query is built with xPDO, but executed directly with PDO. This means there are no objects created to represent the rows in the database.
- Faster chunk processing. If pdoTools can parse the chunk itself, the standard MODX parser is not used.
To achieve this there are two classes in pdoTools. pdoTools is the primary class with general methods for parsing chunks, using placeholders, caching etc. pdoFetch is the class that works with the database. This can be used to build and execute queries, and to return the results.
pdoFetch Class
pdoFetch extends the pdoTools class, so you only need to load the one class depending on what you need to do. To load the pdoFetch class, you can just do this:
<?php
$pdo = $modx->getService('pdoFetch');
After that you can use all the functions, for example getCollection and getChunk. Here is an example on retrieving and processing resources.
<?php
$pdo = $modx->getService('pdoFetch');
$resources = $pdo->getCollection('modResource', array(
'published' => true,
'deleted' => false
), array(
'parents' => '1,5,6,-9',
'includeTVs' => 'tv1, tv2',
'sortby' => 'id',
'sortby' => 'asc',
'limit' => 20,
));
$tpl = '@INLINE <p>[[+id]] - [[+pagetitle]]</p>';
$output = '';
foreach ($resources as $resource) {
$output .= $pdo->getChunk($tpl, $resource);
}
return $output;
All the logic of the pdoResources snippet is actually inside the pdoFetch class, so you can use that in your own snippets.
pdoFetch tries to execute only a single query at a time. So you need to join tables if you want information that is not in the requested table.
The only time when additional queries are made is when the TVs are selected because we need to get their names and default values for proper requests. After that TVs are joined in the main query, so there are no additional queries to get those values.
If you want to filter by TVs, you can use the &where
property for it.
[[!pdoResources?
&parents=`0`
&includeTVs=`tv1`
&where=`{"tv1":"my_value"}`
]]
As TVs using the default value are not stored as values in the database, you need to compare the value to null
when you want to select those resources. So if you want to get all resources where tv2 is set to the default value, you would do it like this:
[[!pdoResources?
&parents=`0`
&includeTVs=`tv2`
&where=`{"tv2":null}`
]]
In the template it will return the right default value for you to use.
Hopefully this explains pdoFetch and how you can use it. To summarise:
- All data is collected in a single query.
- Use table joins for getting additional data from other tables if needed.
- The results are returned as arrays instead of xPDO objects
- If you enable
&checkPermissions
objects will be created to use thecheckPolicy()
method to make sure the user has permissions.
pdoTools Class
The pdoTools class handles chunks and various service methods. You can load it in the same way as pdoFetch.
<?php
$pdo = $modx->getService('pdoTools');
$chunk = $pdo->getChunk('chunkName', array('with' => 'values'));
The getChunk method allow you to load chunks in several different ways.
- Default method as a chunk from the database. Just specify the name of the chunk.
@INLINE <...>
for a chunk that will be generated on the fly.@FILE path/to/file.tpl
for a chunk that will be loaded from a file. For security reasons you can only use files with.tpl
or.html
extensions, and they need to reside within the directory specified as &tplPath.@TEMPLATE
to use an object its template property to parse the entire template for the result. This is only really useful for resources, and allows pdoTools to function as a replacement for the renderResources snippet.
Here's an example of specifying it as @FILE
with a tplPath property:
[[!pdoResources?
&tplPath=`/assets/chunks/`
&tpl=`@FILE dir/file.tpl`
]]
Every snippet based on pdoTools can use chunks this way. That includes pdoResources, getTickets, msProducts and more.
When you use the @INLINE approach you need to be careful, because the placeholders you specify might be processed before the snippet is executed. To help with that pdoTools supports different tags for placeholders using curly braces:
[[!pdoResources?
&parents=`0`
&tpl=`@INLINE <p>{{+id}} - {{+pagetitle}}</p>`
]]
These placeholders will be passed to the snippet without getting processed, and then pdoTools will replace {{ and }} to the normal square brackets. Using this syntax for all @INLINE chunks on a page is important.
When placeholders are parsed by pdoTools, it will try to parse it directly, instead of passing it to the core parser. It can do this for simple tags:
[[+placeholder]]
[[%lexicon]]
[[~id_for_link]]
[[~[[+id]]]]
[[*pagetitle]]
[[*tvfield]]
If you have any nested snippets, chunks or output filters it will load the MODX parser. So if you use output filters the processing will be slower.
Luckily, pdoTools has another trick that allows you to modify the data before processing. This is the &prepareSnippet
property. With &prepareSnippet, you can specify a snippet that should be called before processing the chunk to modify the data. For example:
[[!pdoResources?
&parents=`0`
&tpl=`@INLINE <p>{{+id}} - {{+pagetitle}}</p>`
&prepareSnippet=`cookMyData`
]]
The snippet cookMyData
will have access to a $row
variable with all the fields of a specific row. The snippet can modify this data, and then return a JSON or serialised string with the row data.
Let's add a random number to the end of the pagetitle of each resource in the cookMyData snippet.
<?php
$row['pagetitle'] .= rand();
return json_encode($row);
It's possible to return the data with either json_encode()
, or with serialise()
. By using the prepare snippet, you can throw away all output filters and nested snippets from your chunks to make it faster. Executing one snippet is much faster than parsing lots of different tags in chunks.
In the prepareSnippet you also have access to $modx and $pdoTools to cache data or retrieve other information.
A useful example of how you can use that is the setStore() and getStore() methods on $pdoTools to cache data in memory. For example for Tickets you can highlight users of certain groups with the prepareComments snippet.
[[!TicketComments?
&prepareSnippet=`prepareComments`
]]
<?php
if (empty($row['createdby'])) {return json_encode($row);}
// If we do not have cached groups
if (!$groups = $pdoTools->getStore('groups')) {
$tstart = microtime(true);
$q = $modx->newQuery('modUserGroupMember');
$q->innerJoin('modUserGroup', 'modUserGroup', 'modUserGroupMember.user_group = modUserGroup.id');
$q->select('modUserGroup.name, modUserGroupMember.member');
$q->where(array('modUserGroup.name:!=' => 'Users'));
if ($q->prepare() && $q->stmt->execute()) {
$modx->queryTime += microtime(true) - $tstart;
$modx->executedQueries++;
$groups = array();
while ($tmp = $q->stmt->fetch(PDO::FETCH_ASSOC)) {
$name = strtolower($tmp['name']);
if (!isset($groups[$name])) {
$groups[$name] = array($tmp['member']);
}
else {
$groups[$name][] = $tmp['member'];
}
}
}
foreach ($groups as & $v) {
$v = array_flip($v);
}
// Save groups to cache
$pdoTools->setStore('groups', $groups);
}
$class = '';
if (!empty($row['blocked'])) {
$class = 'blocked';
}
elseif (isset($groups['administrator'][$row['createdby']])) {
$class = 'administrator';
}
$row['class'] = $class;
return json_encode($row);
After that snippet is executed, you can use the class placeholder in the chunk to highlight admins and blocked users. Using the store methods of pdoTools allows you to cache data in memory, it is not written to files, making it very fast for a temporary cache.
So to summarise.
- There are 4 ways of specifying chunks to use.
- The simpler the chunk, the faster it will be processed.
- Using the &prepareSnippet option to replace output filters and other template logic is faster.
Every nested call in a chunk will take more time to process. Logic should be in PHP, not in MODX tags.
Conclusion
Now you know how you can make your site faster with pdoTools. Simplify your chunks and place logic in a special snippet that you call with &prepareSnippet. For me this is the best way to develop traditional sites in MODX.
But pdoTools also has built-in support for a real template engine called Fenom. In the next article about pdoTools we will look at that in more detail.