├── .gitignore ├── LICENSE ├── README.md ├── ajax-template-part.php ├── composer.json ├── inc └── helpers.php ├── js ├── atp.js └── atp.min.js ├── license └── src ├── Cache ├── HandlerInterface.php ├── Provider.php ├── ProviderInterface.php ├── StashDriverProvider.php ├── StashHandler.php └── TransientHandler.php ├── FileSystem.php ├── Loader.php └── Templater.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2016 Giuseppe Mazzapica 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 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ajax_template_part 2 | ================== 3 | 4 | This plugin adds **`ajax_template_part()`** function to WordPress. 5 | 6 | It works like [`get_template_part()`](http://codex.wordpress.org/Function_Reference/get_template_part), 7 | but **ajax powered**. 8 | 9 | #What does "ajax powered" mean? 10 | 11 | When files containing `ajax_template_part()` calls are loaded by WordPress, required 12 | templates are **not** loaded immediately, only when entire page is loaded, an ajax request is sent to 13 | server to get all the templates required, and related content is *pushed* in right place. 14 | 15 | # First question is: 16 | 17 | > Why should I run 2 HTTP requests (the *regular* one plus the ajax one) 18 | > to show my template, when using `get_template_part()` I run only one? 19 | 20 | 21 | **Two reasons:** 22 | 23 | ### 1. Quick response 24 | 25 | Templates may require *some* time to be rendered. Let's imagine a `loop.php` template 26 | that shows a set of posts, each containing a shortcode that renders *things* dynamically. 27 | Requiring that template using *standard* way will slow down page loading. However, sometimes 28 | may be desirable to give a quick response to user that *lands* to the page and *defer* the loading 29 | of slow things (maybe showing a loading UI). 30 | 31 | That is possibly more useful to render "secondary" contents: e.g. load a post content as soon as possible, 32 | while a section with related posts, ads or stuff alike are loaded via ajax in meanwhile. 33 | 34 | ### 2. Cache 35 | 36 | `ajax_template_part()` embed a powerful cache system: *all* the contents loaded via ajax 37 | are cached. That means that using this single, core-alike function is possible to implement 38 | a *deferred* fragment cache system, without having to change existing code, to use additional libraries 39 | or to setup anything. 40 | More on plugin cache system [later in this page](#cache). 41 | 42 | ---- 43 | 44 | #Requirements 45 | 46 | - PHP 5.4+ 47 | - WordPress 3.9+ 48 | - [Composer](https://getcomposer.org/) to install 49 | 50 | ---- 51 | 52 | #Installation 53 | 54 | The plugin is a Composer package and can be installed in plugin directory via: 55 | 56 | ``` bash 57 | composer create-project gmazzap/ajax-template-part --no-dev 58 | ``` 59 | 60 | 61 | ---- 62 | 63 | #How to use 64 | 65 | `ajax_template_part()` works just like `get_template_part()` 66 | 67 | Accepts 2 arguments: 68 | 69 | - `$slug` The slug name for the generic template (required) 70 | - `$name` The name of the *specialized* template (optional) 71 | 72 | e.g. if the function is called like so: 73 | 74 | ``` php 75 | ajax_template_part( 'content' ); 76 | ``` 77 | 78 | it will look for `content.php`, first in child theme folder (if any), then in parent theme. 79 | 80 | If called like so: 81 | 82 | ``` php 83 | ajax_template_part( 'content', 'page' ); 84 | ``` 85 | 86 | it will load via ajax the first that exists among, in order: 87 | 88 | - `content-page.php` in child theme 89 | - `content-page.php` in parent theme 90 | - `content.php` in child theme 91 | - `content.php` in parent theme 92 | 93 | ## Show content while loading 94 | 95 | By default, where the function is called inside containing template, nothing appear until the 96 | ajax template is not loaded. 97 | However, is possible to show *something*: a spinner image, a loading message, default text.. 98 | 99 | That can be done in 2 ways: 100 | 101 | - via filter 102 | - using the **`ajax_template_part_content()`** function. 103 | 104 | ### Via filter 105 | 106 | Plugin provides the filter **`"ajax_template_loading_content"`** and whatever is returned by hooking 107 | callbacks is used as temporary content until required template is loaded. 108 | Using this technique is possible to use a different contents for different calls thanks to the fact 109 | that hooking callbacks receive 4 arguments 110 | 111 | - the current content, that is an empty string, by default 112 | - the "name" of the template required, that is 1st argument passed to function 113 | - the "slug" of the template required, that is 2nd argument passed to function 114 | - current main query object 115 | 116 | ### Via `ajax_template_part_content()` 117 | 118 | This function works in a pretty similar way to `ajax_template_part()`, but takes 3 arguments: 119 | whatever is passed as 1st argument is shown as temporary content, the other 2 arguments are the same 120 | of `ajax_template_part()`. 121 | Note that content passed to this function is not filtered using `"ajax_template_loading_content"` hook. 122 | 123 | ## Temporary container class 124 | 125 | When a temporary content is set, via filter or using `ajax_template_part_content()`, it is added wrapped 126 | inside a `
` tag. 127 | 128 | Is possible to set HTML class attribute for this container using 129 | **`"ajax_template_loading_class"`** filter hook. 130 | 131 | Callbacks hooking this filter receive same 4 arguments passed by `"ajax_template_loading_content"`. 132 | 133 | Note that `
` container is always added to page, even when no temporary content is used, but 134 | by default it is hidden using in-line CSS, but using `"ajax_template_loading_class"` filter, is 135 | possible to add classes to the `
` and so be able to style it via CSS: among other things is possible 136 | to use a *loader image* by setting it as background image CSS property of a class 137 | added via this filter. 138 | 139 | 140 | ## Nested calls 141 | 142 | If in template loaded using `ajax_template_part()` there are additional calls to same function, 143 | *nested* templates are loaded as expected, and in the same ajax request: don't expect 144 | *another* ajax request triggered when *parent* ajax-required template as been loaded. 145 | 146 | That applies in the exact manner to any `get_template_part()` call inside ajax loaded templates. 147 | 148 | ## Stay safe 149 | 150 | To put `ajax_template_part()` calls in your templates makes your site *require* this plugin is 151 | installed, and active, otherwise you'll get fatal error because of function not declared. 152 | 153 | To avoid such problems, e.g. if you deactivate plugin by accident, can be a good idea put this on top 154 | of your `functions.php`: 155 | 156 | ``` php 157 | if ( ! function_exists( 'ajax_template_part' ) ) { 158 | function ajax_template_part( $name = '', $slug = '' ) { 159 | return get_template_part( $name, $slug ); 160 | } 161 | } 162 | if ( ! function_exists( 'ajax_template_part_content' ) ) { 163 | function ajax_template_part_content( $content = '', $name = '', $slug = '' ) { 164 | return get_template_part( $name, $slug ); 165 | } 166 | } 167 | ``` 168 | 169 | In this way your theme will *gracefully degrade* to `get_template_part` if plugin is not active for 170 | any reason. 171 | 172 | ---- 173 | 174 | # Cache 175 | 176 | Ajax template loading and content generation may be heavy, so plugin *needs* a way to cache them. 177 | 178 | Cache is active by default when the constant `WP_DEBUG` is set to `FALSE`, this should be a pretty 179 | common way to turn it on for production and off in locale / development environments. 180 | 181 | However, using the **`"ajax_template_cache"`** filter is possible to customize when enable or disable caching. 182 | Only argument this hook passes to hooking callbacks is the current cache status and have to return 183 | a boolean: `TRUE` means cache active. 184 | 185 | This plugin can work with different types of caches: 186 | 187 | - if in the system is installed an external object cache, then this plugin will use that, nothing is left to do. 188 | - if no external object cache is in use, this plugin uses [Stash](http://www.stashphp.com) to cache data. 189 | This library can make use of different "drivers": FileSystem, APC, Memcached, Redis... 190 | By default plugin uses FileSystem driver and *no* configuration is required to use that. 191 | However is possible to use any supported driver, if system has the requirements. 192 | 193 | ## Use an alternative cache driver 194 | 195 | To use a different driver there are 2 filters available: 196 | 197 | - **`"ajax_template_cache_driver"`**: hooking callback must return the fully qualified name 198 | of the class to use, one among: 199 | - `Stash\Driver\Sqlite` 200 | - `Stash\Driver\Memcache` 201 | - `Stash\Driver\APC` 202 | - `Stash\Driver\Redis` 203 | - `Stash\Driver\Composite` 204 | 205 | please refer to [Stash documentation](http://www.stashphp.com/Drivers.html) for details. 206 | 207 | - **`"ajax_template_{$driver}_driver_conf"`** is the filter to be used to configure the chosen driver. 208 | Hooking callback must return the configuration array, see Stash documentation for details. 209 | 210 | As example, configuration to use Memcache driver should be something like this: 211 | 212 | ```php 213 | add_filter( 'ajax_template_cache_driver', function() { 214 | return '\Stash\Driver\Memcache'; 215 | }); 216 | 217 | add_filter( "ajax_template_memcache_driver_conf", function() { 218 | return [ 'servers' => ['127.0.0.1', '11211'] ]; 219 | }); 220 | ``` 221 | 222 | ## Cache expiration 223 | 224 | By default contents are cached for 1 hour. Consider that if content (e.g. posts) is updated 225 | and the old content is cached, updated content will not be shown until cache expires. 226 | 227 | Is possible to change default expiration time using `"ajax_template_cache_ttl"` filter hook. 228 | Hooking callbacks receives and have to return cache "time to live" value in seconds. 229 | Note that setting a value under 30 seconds will be skipped and default will be used. 230 | 231 | If you need to disable cache don't use this filter but `"ajax_template_cache"` or set `WP_DEBUG` to true 232 | (not recommended for production, highly recommended for development environments). 233 | 234 | --------- 235 | 236 | #License 237 | 238 | This plugin is released under MIT license. 239 | -------------------------------------------------------------------------------- /ajax-template-part.php: -------------------------------------------------------------------------------- 1 | =5.5", 26 | "composer/installers": "~1.0", 27 | "tedivm/stash": "~0.14.0" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "GM\\ATP\\": "src/" 32 | }, 33 | "files": [ 34 | "inc/helpers.php" 35 | ] 36 | }, 37 | "config": { 38 | "optimize-autoloader": true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /inc/helpers.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace GM\ATP; 12 | 13 | use Stash\DriverList; 14 | use Stash\Interfaces\DriverInterface; 15 | use Stash\Pool; 16 | 17 | /** 18 | * Plugin activation callback. 19 | */ 20 | function activate() 21 | { 22 | wp_schedule_event(time(), 'daily', 'ajaxtemplatepart_cache_purge'); 23 | } 24 | 25 | /** 26 | * Plugin deactivation callback. 27 | */ 28 | function deactivate() 29 | { 30 | $stash = get_stash(); 31 | $stash instanceof Pool and $stash->flush(); 32 | $timestamp = wp_next_scheduled('ajaxtemplatepart_cache_purge'); 33 | $timestamp and wp_unschedule_event($timestamp, 'ajaxtemplatepart_cache_purge'); 34 | } 35 | 36 | /** 37 | * @param string $name 38 | * @param string $slug 39 | * @param string $content 40 | */ 41 | function template_part($name, $slug = '', $content = '') 42 | { 43 | if (defined('DOING_AJAX') && DOING_AJAX) { 44 | return \get_template_part($name, $slug); 45 | } 46 | 47 | $path = dirname(__DIR__).'/ajax-template-part.php'; 48 | $templater = new Templater($GLOBALS['wp'], $GLOBALS['wp_query'], $path); 49 | $templater->addJs()->addHtml($name, $slug, $content); 50 | } 51 | 52 | /** 53 | * AJAX callback. 54 | */ 55 | function ajax_callback() 56 | { 57 | if (! defined('DOING_AJAX') || ! DOING_AJAX) { 58 | return; 59 | } 60 | 61 | $loader = new Loader(); 62 | 63 | /** 64 | * No caching when WP_DEBUG is true, but can be filtered via "ajax_template_cache" hook. 65 | * If external object cache is active use that (via Transients API) otherwise 66 | * use Stash, by default with FileSystem driver, but driver and its options can be customized 67 | * via "ajax_template_cache_driver" and "ajax_template_{$driver}_driver_conf" filter hooks. 68 | */ 69 | $provider = new Cache\Provider(); 70 | $handler = $provider->isEnabled() ? get_cache_handler() : null; 71 | 72 | if ($handler) { 73 | $provider->setHandler($handler); 74 | $loader->setCacheProvider($provider); 75 | } 76 | 77 | $loader->getData(); 78 | 79 | exit(); 80 | } 81 | 82 | /** 83 | * @return \Stash\Pool|void 84 | */ 85 | function get_stash() 86 | { 87 | $drivers = DriverList::getAvailableDrivers(); 88 | $provider = new Cache\StashDriverProvider(new FileSystem(), $drivers); 89 | $class = $provider->getDriverClass(); 90 | if (! class_exists($class)) { 91 | return; 92 | } 93 | 94 | $driver = new $class(); 95 | if ($driver instanceof DriverInterface) { 96 | $options = $provider->getDriverOptions($class); 97 | $driver->setOptions($options); 98 | 99 | return new Pool($driver); 100 | } 101 | } 102 | 103 | /** 104 | * @return \GM\ATP\Cache\HandlerInterface 105 | */ 106 | function get_cache_handler() 107 | { 108 | static $handler; 109 | if (! is_null($handler)) { 110 | return $handler; 111 | } 112 | 113 | $transient = new Cache\TransientHandler(); 114 | if ($transient->isAvailable()) { 115 | $handler = $transient; 116 | 117 | return $transient; 118 | } 119 | 120 | $handler = new Cache\StashHandler(get_stash()); 121 | 122 | return $handler; 123 | } 124 | 125 | /** 126 | * Purge cache. 127 | */ 128 | function cache_purge() 129 | { 130 | $provider = new Cache\Provider(); 131 | $handler = $provider->isEnabled() ? get_cache_handler() : null; 132 | $handler instanceof Cache\StashHandler and $handler->getStash()->purge(); 133 | } 134 | -------------------------------------------------------------------------------- /js/atp.js: -------------------------------------------------------------------------------- 1 | (function ($, ATP) { 2 | 3 | "use strict" 4 | 5 | ATP.methods = {}; 6 | ATP.map = {ids: [], spans: {}, slugs: {}, posts: {}}; 7 | 8 | ATP.methods.checkResponse = function (response) { 9 | return typeof response.success !== 'undefined' 10 | && response.success 11 | && typeof response.data === 'object'; 12 | }; 13 | 14 | ATP.methods.onError = function () { 15 | $(ATP.map.ids).each(function (id, el) { 16 | $('#' + el).remove(); 17 | }); 18 | $(document).trigger('ajax_template_part_error', ATP.map); 19 | $(document).trigger('ajax_template_part_done'); 20 | 21 | }; 22 | 23 | ATP.methods.onSuccess = function (data) { 24 | $.each(ATP.map.ids, function (id, el) { 25 | var $el = $('#' + el); 26 | if ($el.length && typeof data[el] === 'string') { 27 | $el.replaceWith(data[el]).show(); 28 | } else { 29 | $el.remove(); 30 | } 31 | }); 32 | $(document).trigger('ajax_template_part_success', ATP.map); 33 | $(document).trigger('ajax_template_part_done'); 34 | }; 35 | 36 | ATP.methods.callAjax = function () { 37 | return $.ajax({ 38 | url: ATP.info.ajax_url, 39 | dataType: 'json', 40 | type: 'POST', 41 | data: { 42 | action: 'ajaxtemplatepart', 43 | query_data: ATP.info.query_data, 44 | files_data: ATP.map.slugs, 45 | posts_data: ATP.map.posts 46 | } 47 | }); 48 | }; 49 | 50 | ATP.methods.parseDom = function () { 51 | $('span[data-ajaxtemplate]').each(function (i, el) { 52 | var $el = $(el); 53 | var id = $el.attr('id'); 54 | var rawData = decodeURIComponent($el.data('ajaxtemplate').replace(/\+/g, ' ')); 55 | var templateData = $.parseJSON(rawData); 56 | var name = templateData.name; 57 | var slug = templateData.slug; 58 | var post = $el.data('post'); 59 | if (typeof id === 'string' && id !== '' && typeof name === 'string' && name !== '') { 60 | if (typeof slug !== 'string') { 61 | slug = false; 62 | } 63 | if (Number(post) <= 0) { 64 | post = false; 65 | } 66 | ATP.map.ids.push(id); 67 | ATP.map.spans[id] = $el; 68 | ATP.map.slugs[id] = [name, slug]; 69 | ATP.map.posts[id] = post; 70 | } 71 | $el.data('ajaxtemplate', null); 72 | }); 73 | }; 74 | 75 | ATP.methods.update = function () { 76 | if (ATP.map.ids.length < 1) { 77 | return false; 78 | } 79 | ATP.methods 80 | .callAjax() 81 | .done(function (response) { 82 | if (ATP.methods.checkResponse(response)) { 83 | ATP.methods.onSuccess(response.data); 84 | } else { 85 | ATP.methods.onError(); 86 | } 87 | }) 88 | .fail(function () { 89 | ATP.methods.onError(); 90 | }); 91 | }; 92 | 93 | $(document).ready(function () { 94 | ATP.methods.parseDom(); 95 | ATP.methods.update(); 96 | }); 97 | 98 | $(document).on('ajax_template_part_done', function () { 99 | ATP.map = {ids: [], spans: {}, slugs: {}, posts: {}}; 100 | }); 101 | 102 | })(jQuery, AjaxTemplatePartData); 103 | -------------------------------------------------------------------------------- /js/atp.min.js: -------------------------------------------------------------------------------- 1 | (function(b,a){a.methods={};a.map={ids:[],spans:{},slugs:{},posts:{}};a.methods.checkResponse=function(c){return typeof c.success!=="undefined"&&c.success&&typeof c.data==="object"};a.methods.onError=function(){b(a.map.ids).each(function(d,c){b("#"+c).remove()});b(document).trigger("ajax_template_part_error",a.map);b(document).trigger("ajax_template_part_done")};a.methods.onSuccess=function(c){b.each(a.map.ids,function(f,e){var d=b("#"+e);if(d.length&&typeof c[e]==="string"){d.replaceWith(c[e]).show()}else{d.remove()}});b(document).trigger("ajax_template_part_success",a.map);b(document).trigger("ajax_template_part_done")};a.methods.callAjax=function(){return b.ajax({url:a.info.ajax_url,dataType:"json",type:"POST",data:{action:"ajaxtemplatepart",query_data:a.info.query_data,files_data:a.map.slugs,posts_data:a.map.posts}})};a.methods.parseDom=function(){b("span[data-ajaxtemplate]").each(function(j,f){var l=b(f);var e=l.attr("id");var c=decodeURIComponent(l.data("ajaxtemplate").replace(/\+/g," "));var g=b.parseJSON(c);var d=g.name;var h=g.slug;var k=l.data("post");if(typeof e==="string"&&e!==""&&typeof d==="string"&&d!==""){if(typeof h!=="string"){h=false}if(Number(k)<=0){k=false}a.map.ids.push(e);a.map.spans[e]=l;a.map.slugs[e]=[d,h];a.map.posts[e]=k}l.data("ajaxtemplate",null)})};a.methods.update=function(){if(a.map.ids.length<1){return false}a.methods.callAjax().done(function(c){if(a.methods.checkResponse(c)){a.methods.onSuccess(c.data)}else{a.methods.onError()}}).fail(function(){a.methods.onError()})};b(document).ready(function(){a.methods.parseDom();a.methods.update()});b(document).on("ajax_template_part_done",function(){a.map={ids:[],spans:{},slugs:{},posts:{}}})})(jQuery,AjaxTemplatePartData); -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2016 Giuseppe Mazzapica 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 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/Cache/HandlerInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace GM\ATP\Cache; 12 | 13 | /** 14 | * @author Giuseppe Mazzapica 15 | * @license http://opensource.org/licenses/MIT MIT 16 | * @package ATP 17 | */ 18 | interface HandlerInterface 19 | { 20 | /** 21 | * @param $key 22 | */ 23 | public function get($key); 24 | 25 | /** 26 | * @param string $key 27 | * @param mixed $value 28 | * @param int $expiration 29 | */ 30 | public function set($key, $value, $expiration); 31 | 32 | /** 33 | * @param string $key 34 | */ 35 | public function clear($key); 36 | 37 | /** 38 | * @return bool 39 | */ 40 | public function isAvailable(); 41 | } 42 | -------------------------------------------------------------------------------- /src/Cache/Provider.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace GM\ATP\Cache; 12 | 13 | /** 14 | * @author Giuseppe Mazzapica 15 | * @license http://opensource.org/licenses/MIT MIT 16 | * @package ATP 17 | */ 18 | class Provider implements ProviderInterface 19 | { 20 | /** 21 | * @var \GM\ATP\Cache\HandlerInterface 22 | */ 23 | private $handler; 24 | 25 | /** 26 | * @var string 27 | */ 28 | private $id; 29 | 30 | /** 31 | * @var int 32 | */ 33 | private $ttl; 34 | 35 | /** 36 | * @return \GM\ATP\Cache\HandlerInterface 37 | */ 38 | public function getHandler() 39 | { 40 | return $this->handler; 41 | } 42 | 43 | /** 44 | * @param \GM\ATP\Cache\HandlerInterface $handler 45 | */ 46 | public function setHandler(HandlerInterface $handler) 47 | { 48 | $this->handler = $handler; 49 | } 50 | 51 | /** 52 | * @param array $id1 53 | * @param array $id2 54 | * @return mixed 55 | */ 56 | public function get(array $id1, array $id2) 57 | { 58 | $key = $this->id($id1, $id2); 59 | 60 | return $this->getHandler()->get($key); 61 | } 62 | 63 | /** 64 | * @param array $value 65 | * @param array $id1 66 | * @param array $id2 67 | * @return mixed 68 | */ 69 | public function set(array $value, array $id1, array $id2) 70 | { 71 | $key = $this->id($id1, $id2); 72 | 73 | return $this->getHandler()->set($key, $value, $this->ttl()); 74 | } 75 | 76 | /** 77 | * @return bool 78 | */ 79 | public function isEnabled() 80 | { 81 | return apply_filters('ajax_template_cache', (defined('WP_DEBUG') && ! WP_DEBUG)); 82 | } 83 | 84 | /** 85 | * @return bool 86 | */ 87 | public function shouldCache() 88 | { 89 | if ($this->isEnabled()) { 90 | $handler = $this->getHandler(); 91 | 92 | return $handler instanceof HandlerInterface && $handler->isAvailable(); 93 | } 94 | 95 | return false; 96 | } 97 | 98 | /** 99 | * @param array $id1 100 | * @param array $id2 101 | * @return string 102 | */ 103 | private function id(array $id1, array $id2) 104 | { 105 | if (! is_null($this->id)) { 106 | return $this->id; 107 | } 108 | 109 | $a = array_filter($id1); 110 | $b = array_filter($id2); 111 | ksort($a); 112 | ksort($b); 113 | $this->id = 'GM_ATP_'.md5(serialize($a).serialize($b)); 114 | 115 | return $this->id; 116 | } 117 | 118 | /** 119 | * @return int 120 | */ 121 | private function ttl() 122 | { 123 | if (is_null($this->ttl)) { 124 | $ttl = apply_filters('ajax_template_cache_ttl', HOUR_IN_SECONDS); 125 | $this->ttl = is_scalar($ttl) && (int)$ttl > 30 ? (int)$ttl : HOUR_IN_SECONDS; 126 | } 127 | 128 | return $this->ttl; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Cache/ProviderInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace GM\ATP\Cache; 12 | 13 | /** 14 | * @author Giuseppe Mazzapica 15 | * @license http://opensource.org/licenses/MIT MIT 16 | * @package ATP 17 | */ 18 | interface ProviderInterface 19 | { 20 | /** 21 | * @param array $id1 22 | * @param array $id2 23 | */ 24 | public function get(array $id1, array $id2); 25 | 26 | /** 27 | * @param array $value 28 | * @param array $id1 29 | * @param array $id2 30 | */ 31 | public function set(array $value, array $id1, array $id2); 32 | 33 | /** 34 | * @return bool 35 | */ 36 | public function isEnabled(); 37 | 38 | /** 39 | * @return bool 40 | */ 41 | public function shouldCache(); 42 | 43 | /** 44 | * @return HandlerInterface 45 | */ 46 | public function getHandler(); 47 | 48 | /** 49 | * @param \GM\ATP\Cache\HandlerInterface $handler 50 | */ 51 | public function setHandler(HandlerInterface $handler); 52 | } 53 | -------------------------------------------------------------------------------- /src/Cache/StashDriverProvider.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace GM\ATP\Cache; 12 | 13 | use GM\ATP\FileSystem; 14 | use Stash\Driver\FileSystem as FileSystemDriver; 15 | use Stash\Interfaces\DriverInterface; 16 | 17 | /** 18 | * @author Giuseppe Mazzapica 19 | * @license http://opensource.org/licenses/MIT MIT 20 | * @package ATP 21 | */ 22 | class StashDriverProvider 23 | { 24 | /** 25 | * @var \GM\ATP\FileSystem 26 | */ 27 | private $fs; 28 | 29 | /** 30 | * @var array 31 | */ 32 | private $drivers; 33 | 34 | /** 35 | * @param \GM\ATP\FileSystem $fs 36 | * @param array $drivers 37 | */ 38 | public function __construct(FileSystem $fs, array $drivers = []) 39 | { 40 | $this->fs = $fs; 41 | $this->drivers = $drivers; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function getDriverClass() 48 | { 49 | $driver = apply_filters('ajax_template_cache_driver', FileSystemDriver::class); 50 | 51 | if ( 52 | ! is_subclass_of($driver, DriverInterface::class, true) 53 | || $driver === FileSystemDriver::class 54 | ) { 55 | return FileSystemDriver::class; 56 | } 57 | 58 | /** @var callable $cb */ 59 | $cb = [$driver, 'isAvailable']; 60 | 61 | return $cb() ? $driver : FileSystemDriver::class; 62 | } 63 | 64 | /** 65 | * @param $driver_class 66 | * @return string 67 | */ 68 | public function getDriverName($driver_class) 69 | { 70 | return array_search($driver_class, $this->drivers, true); 71 | } 72 | 73 | /** 74 | * @param $driver_class 75 | * @return array 76 | */ 77 | public function getDriverOptions($driver_class) 78 | { 79 | $driver_name = $this->getDriverName($driver_class); 80 | if (empty($driver_name) || $driver_name === 'FileSystem') { 81 | $path = $this->fs->getFolder(); 82 | 83 | return $path ? ['path' => $path] : []; 84 | } 85 | 86 | $name = strtolower($driver_name); 87 | 88 | return apply_filters("ajax_template_{$name}_driver_conf", []); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Cache/StashHandler.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace GM\ATP\Cache; 12 | 13 | use Stash\Interfaces\ItemInterface; 14 | use Stash\Interfaces\PoolInterface; 15 | 16 | /** 17 | * @author Giuseppe Mazzapica 18 | * @license http://opensource.org/licenses/MIT MIT 19 | * @package ATP 20 | */ 21 | class StashHandler implements HandlerInterface 22 | { 23 | /** 24 | * @var \Stash\Interfaces\PoolInterface 25 | */ 26 | private $stash; 27 | 28 | /** 29 | * @param \Stash\Interfaces\PoolInterface $pool 30 | */ 31 | public function __construct(PoolInterface $pool) 32 | { 33 | return $this->stash = $pool; 34 | } 35 | 36 | /** 37 | * @param string $key 38 | * @return bool|\Exception 39 | */ 40 | public function clear($key) 41 | { 42 | try { 43 | $item = $this->getItem($key); 44 | if (! $item instanceof ItemInterface) { 45 | return false; 46 | } 47 | 48 | $item->clear(); 49 | 50 | return true; 51 | } catch (\Exception $e) { 52 | return false; 53 | } 54 | } 55 | 56 | /** 57 | * @param $key 58 | * @return mixed 59 | */ 60 | public function get($key) 61 | { 62 | try { 63 | $item = $this->getItem($key); 64 | if (! $item instanceof ItemInterface) { 65 | return false; 66 | } 67 | $cached = $item->get(); 68 | 69 | return $item->isMiss() ? false : $cached; 70 | } catch (\Exception $e) { 71 | return false; 72 | } 73 | } 74 | 75 | /** 76 | * @param string $key 77 | * @param mixed $value 78 | * @param int $expiration 79 | * @return bool 80 | */ 81 | public function set($key, $value, $expiration) 82 | { 83 | try { 84 | $item = $this->getItem($key); 85 | if (! $item instanceof ItemInterface) { 86 | return false; 87 | } 88 | $item->clear(); 89 | $item->lock(); 90 | 91 | return $item->set($value, $expiration); 92 | } catch (\Exception $e) { 93 | return false; 94 | } 95 | } 96 | 97 | /** 98 | * @return bool 99 | */ 100 | public function isAvailable() 101 | { 102 | return call_user_func([get_class($this->getStash()->getDriver()), 'isAvailable']); 103 | } 104 | 105 | /** 106 | * @return \Stash\Interfaces\PoolInterface 107 | */ 108 | public function getStash() 109 | { 110 | return $this->stash; 111 | } 112 | 113 | /** 114 | * @param $key 115 | * @return \Stash\Interfaces\ItemInterface|null 116 | */ 117 | private function getItem($key) 118 | { 119 | try { 120 | return $this->stash->getItem($key); 121 | } catch (\Exception $e) { 122 | return; 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Cache/TransientHandler.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace GM\ATP\Cache; 12 | 13 | /** 14 | * @author Giuseppe Mazzapica 15 | * @license http://opensource.org/licenses/MIT MIT 16 | * @package ATP 17 | */ 18 | class TransientHandler implements HandlerInterface 19 | { 20 | /** 21 | * @param string $key 22 | * @return bool 23 | */ 24 | public function clear($key) 25 | { 26 | return delete_transient($key); 27 | } 28 | 29 | /** 30 | * @param string $key 31 | * @return bool 32 | */ 33 | public function get($key) 34 | { 35 | return get_transient($key) ? : false; 36 | } 37 | 38 | /** 39 | * @param string $key 40 | * @param mixed $value 41 | * @param int $expiration 42 | * @return bool 43 | */ 44 | public function set($key, $value, $expiration) 45 | { 46 | return set_transient($key, $value, $expiration); 47 | } 48 | 49 | /** 50 | * @return bool 51 | */ 52 | public function isAvailable() 53 | { 54 | return apply_filters('ajax_template_force_transient', wp_using_ext_object_cache()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/FileSystem.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace GM\ATP; 12 | 13 | /** 14 | * @author Giuseppe Mazzapica 15 | * @license http://opensource.org/licenses/MIT MIT 16 | * @package ATP 17 | */ 18 | class FileSystem 19 | { 20 | /** 21 | * @return string 22 | */ 23 | public function getFolder() 24 | { 25 | $upload = wp_upload_dir(); 26 | $path = trailingslashit($upload['basedir']).'ajax_query_template/cache'; 27 | 28 | return wp_mkdir_p($path) ? $path : ''; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Loader.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace GM\ATP; 12 | 13 | /** 14 | * @author Giuseppe Mazzapica 15 | * @license http://opensource.org/licenses/MIT MIT 16 | * @package ATP 17 | */ 18 | class Loader 19 | { 20 | /** 21 | * @var \GM\ATP\Cache\Provider 22 | */ 23 | private $cache; 24 | 25 | /** 26 | * @var string[] 27 | */ 28 | private $output = []; 29 | 30 | /** 31 | * @var array 32 | */ 33 | private $query_data = []; 34 | 35 | /** 36 | * @var array 37 | */ 38 | private $templates_data = []; 39 | 40 | /** 41 | * @var array 42 | */ 43 | private $posts_data = []; 44 | 45 | /** 46 | * Return data from AJAX request. 47 | */ 48 | public function getData() 49 | { 50 | if (! defined('DOING_AJAX') || ! DOING_AJAX) { 51 | return; 52 | } 53 | 54 | $this->parseRequest(); 55 | 56 | if (empty($this->templates_data)) { 57 | wp_send_json_error(); 58 | } 59 | 60 | $this->getCache(); 61 | $this->loadTemplates(); 62 | $this->setCache(); 63 | $this->output ? wp_send_json_success($this->output) : wp_send_json_error($this->output); 64 | } 65 | 66 | /** 67 | * @param \GM\ATP\Cache\Provider $cache 68 | */ 69 | public function setCacheProvider(Cache\Provider $cache) 70 | { 71 | $this->cache = $cache; 72 | } 73 | 74 | /** 75 | * @return mixed 76 | */ 77 | public function getCacheProvider() 78 | { 79 | return $this->cache; 80 | } 81 | 82 | /** 83 | * Parse HTTP request and collect request data. 84 | */ 85 | private function parseRequest() 86 | { 87 | $request = filter_input_array(INPUT_POST, [ 88 | 'files_data' => [ 89 | 'filter' => FILTER_UNSAFE_RAW, 90 | 'flags' => FILTER_REQUIRE_ARRAY, 91 | ], 92 | 'query_data' => [ 93 | 'filter' => FILTER_UNSAFE_RAW, 94 | 'flags' => FILTER_REQUIRE_ARRAY, 95 | ], 96 | 'posts_data' => [ 97 | 'filter' => FILTER_SANITIZE_NUMBER_INT, 98 | 'flags' => FILTER_REQUIRE_ARRAY, 99 | ] 100 | ]); 101 | 102 | $this->query_data = $request['query_data'] ? : []; 103 | $this->templates_data = $request['files_data']; 104 | $posts_data = array_filter(array_unique($request['posts_data'])); 105 | $this->posts_data = (count($posts_data) > 1) ? $request['posts_data'] : []; 106 | } 107 | 108 | /** 109 | * Load template parts, setting up context. 110 | */ 111 | private function loadTemplates() 112 | { 113 | $this->buildGlobals(); 114 | $this->output = []; 115 | foreach ($this->templates_data as $id => $template) { 116 | if (strpos($id, 'ajaxtemplate-') !== 0) { 117 | continue; 118 | } 119 | $data = $this->checkTemplateData($template); 120 | if (! $data) { 121 | $this->output[$id] = $this->loadTemplate($data, $id); 122 | } 123 | } 124 | } 125 | 126 | /** 127 | * Build global context. 128 | */ 129 | private function buildGlobals() 130 | { 131 | global $wp, $wp_query, $wp_the_query, $wp_did_header; 132 | $wp_did_header = 1; 133 | $wp->init(); 134 | $wp->query_vars = $this->query_data; 135 | $wp->query_posts(); 136 | $wp->register_globals(); 137 | isset($wp_the_query) or $wp_the_query = $wp_query; 138 | } 139 | 140 | /** 141 | * Sanitize template parts data from request. 142 | * 143 | * @param $template 144 | * @return array 145 | */ 146 | private function checkTemplateData($template) 147 | { 148 | $data = array_values((array)$template); 149 | if (empty($data) || ! is_string($data[0])) { 150 | return []; 151 | } 152 | $args = [filter_var($data[0], FILTER_SANITIZE_STRING)]; 153 | if (! empty($data) && is_string($data[1])) { 154 | $args[] = filter_var($data[1], FILTER_SANITIZE_STRING); 155 | } 156 | 157 | return array_filter($args); 158 | } 159 | 160 | /** 161 | * Load a single template part, by calling get_template_part(). 162 | * 163 | * @param $args 164 | * @param $id 165 | * @return string 166 | */ 167 | private function loadTemplate($args, $id) 168 | { 169 | if (isset($this->posts_data[$id]) && (int)$this->posts_data[$id] > 0) { 170 | $GLOBALS['post'] = get_post($this->posts_data[$id]); 171 | setup_postdata($GLOBALS['post']); 172 | } 173 | ob_start(); 174 | @call_user_func_array('get_template_part', $args); 175 | $result = ob_get_clean(); 176 | 177 | return $result; 178 | } 179 | 180 | /** 181 | * Get cached data if available. 182 | */ 183 | private function getCache() 184 | { 185 | if (! $this->shouldCache()) { 186 | return; 187 | } 188 | if (! empty($this->posts_data)) { 189 | $this->query_data['posts_data'] = $this->posts_data; 190 | } 191 | $cached = $this->getCacheProvider()->get($this->templates_data, $this->query_data); 192 | if (! empty($cached) && is_array($cached)) { 193 | wp_send_json_success($cached); 194 | } 195 | } 196 | 197 | /** 198 | * Stores data in cache on shutdown. 199 | */ 200 | private function setCache() 201 | { 202 | if (! $this->shouldCache() || empty($this->output)) { 203 | return; 204 | } 205 | 206 | add_action('shutdown', function () { 207 | $this->getCacheProvider()->set($this->output, $this->templates_data, $this->query_data); 208 | }); 209 | } 210 | 211 | /** 212 | * Is cache available and enabled? 213 | * 214 | * @return bool 215 | */ 216 | private function shouldCache() 217 | { 218 | $provider = $this->getCacheProvider(); 219 | 220 | return $provider instanceof Cache\Provider && $provider->shouldCache(); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/Templater.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace GM\ATP; 12 | 13 | /** 14 | * @author Giuseppe Mazzapica 15 | * @license http://opensource.org/licenses/MIT MIT 16 | * @package ATP 17 | */ 18 | class Templater 19 | { 20 | /** 21 | * @var \WP 22 | */ 23 | private $wp; 24 | 25 | /** 26 | * @var \WP_Query 27 | */ 28 | private $query; 29 | 30 | /** 31 | * @var string 32 | */ 33 | private $path; 34 | 35 | /** 36 | * @var bool 37 | */ 38 | private $debug; 39 | 40 | /** 41 | * @param \WP $wp 42 | * @param \WP_Query $query 43 | * @param string $path 44 | */ 45 | public function __construct(\WP $wp, \WP_Query $query, $path) 46 | { 47 | $this->wp = $wp; 48 | $this->query = $query; 49 | $this->path = $path; 50 | $this->debug = defined('WP_DEBUG') && WP_DEBUG; 51 | } 52 | 53 | /** 54 | * @param string $raw_name 55 | * @param string $raw_slug 56 | * @param string $content 57 | * @return static 58 | */ 59 | public function addHtml($raw_name, $raw_slug, $content = '') 60 | { 61 | static $n = 0; 62 | $n++; 63 | $name = filter_var($raw_name, FILTER_SANITIZE_URL); 64 | $slug = filter_var($raw_slug, FILTER_SANITIZE_URL); 65 | $ajaxTemplate = urlencode(json_encode(['name' => $name, 'slug' => $slug])); 66 | 67 | global $post; 68 | $post_attr = ''; 69 | $post instanceof \WP_Post and $post_attr = sprintf(' data-post="%d"', $post->ID); 70 | empty($content) and $content = $this->htmlContent($name, $slug); 71 | 72 | $class = $this->htmlClass($name, $slug); 73 | $attr = empty($content) && empty($class) ? ' style="display:none!important;"' : ''; 74 | $id = md5($name.$slug.$n); 75 | $attr .= " id=\"ajaxtemplate-{$id}\""; 76 | $format = '%s'; 77 | printf($format, $attr, $post_attr, $ajaxTemplate, $class, $content); 78 | 79 | return $this; 80 | } 81 | 82 | /** 83 | * @return static 84 | */ 85 | public function addJs() 86 | { 87 | if (wp_script_is('ajax-template-part', 'queue')) { 88 | return $this; 89 | } 90 | $args = ['ajax-template-part', $this->jsUrl(), ['jquery'], $this->jsVer(), true]; 91 | call_user_func_array('wp_enqueue_script', $args); 92 | $ajax_url = apply_filters('ajax_template_ajax_url', admin_url('admin-ajax.php')); 93 | $data = [ 94 | 'info' => [ 95 | 'ajax_url' => $ajax_url, 96 | 'query_data' => $this->wp->query_vars, 97 | ], 98 | ]; 99 | wp_localize_script('ajax-template-part', 'AjaxTemplatePartData', $data); 100 | 101 | return $this; 102 | } 103 | 104 | /** 105 | * @param string $name 106 | * @param string $slug 107 | * @return string 108 | */ 109 | private function htmlClass($name, $slug) 110 | { 111 | $class = apply_filters('ajax_template_loading_class', '', $name, $slug, $this->query); 112 | if (! is_string($class) || empty($class)) { 113 | return ''; 114 | } 115 | 116 | return sprintf(' class="%s"', esc_attr(trim($class))); 117 | } 118 | 119 | /** 120 | * @param string $name 121 | * @param string $slug 122 | * @return string 123 | */ 124 | private function htmlContent($name, $slug) 125 | { 126 | $content = apply_filters('ajax_template_loading_content', '', $name, $slug, $this->query); 127 | if (! is_string($content) || empty($content)) { 128 | return ''; 129 | } 130 | 131 | return esc_html($content); 132 | } 133 | 134 | /** 135 | * @return string 136 | */ 137 | private function jsUrl() 138 | { 139 | $min = $this->debug ? '' : '.min'; 140 | 141 | return plugins_url("/js/atp{$min}.js", $this->path); 142 | } 143 | 144 | /** 145 | * @return string 146 | */ 147 | private function jsVer() 148 | { 149 | if ($this->debug) { 150 | return (string)time(); 151 | } 152 | 153 | return @filemtime(dirname($this->path).'/js/atp.min.js') ? : null; 154 | } 155 | } 156 | --------------------------------------------------------------------------------