Skip to content

Commit f268539

Browse files
author
Alannah Kearney
committed
Added caching preferences. Added 'convertEmptyElementsToString' transformer. Added 'cacheable' page type to Blueprints > Pages New and Edit interface.
1 parent a2f42ab commit f268539

1 file changed

Lines changed: 326 additions & 51 deletions

File tree

extension.driver.php

Lines changed: 326 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,67 +4,342 @@
44

55
use Symphony\ApiFramework\Lib;
66

7-
class extension_api_framework extends Extension
8-
{
9-
public function getSubscribedDelegates()
7+
// This file is included automatically in the composer autoloader, however,
8+
// Symphony might try to include it again which would cause a fatal error.
9+
// Check if the class already exists before declaring it again.
10+
if (!class_exists("\\Extension_API_Framework")) {
11+
class Extension_API_Framework extends Extension
1012
{
11-
return[
13+
const CACHE_DURATION_MINUTE = "minute";
14+
const CACHE_DURATION_DAY = "day";
15+
const CACHE_DURATION_HOUR= "hour";
16+
const CACHE_DURATION_WEEK = "week";
17+
18+
const CACHE_ENABLED = 'yes';
19+
const CACHE_DISABLED = 'no';
20+
21+
public function getSubscribedDelegates()
22+
{
23+
return [
24+
[
25+
'page' => '/all/',
26+
'delegate' => 'ModifySymphonyLauncher',
27+
'callback' => 'setJSONLauncher'
28+
],
29+
[
30+
'page' => '/frontend/',
31+
'delegate' => 'APIFrameworkJSONRendererAppendTransformations',
32+
'callback' => 'appendTransformations'
33+
],
34+
[
35+
'page' => '/system/preferences/',
36+
'delegate' => 'AddCustomPreferenceFieldsets',
37+
'callback' => 'appendPreferences'
38+
],
1239
[
13-
'page' => '/all/',
14-
'delegate' => 'ModifySymphonyLauncher',
15-
'callback' => 'setJSONLauncher'
40+
'page' => '/system/preferences/',
41+
'delegate' => 'Save',
42+
'callback' => 'savePreferences'
1643
],
1744
[
18-
'page' => '/frontend/',
19-
'delegate' => 'APIFrameworkJSONRendererAppendTransformations',
20-
'callback' => 'appendTransformations'
45+
'page' => '/blueprints/pages/',
46+
'delegate' => 'AppendPageContent',
47+
'callback' => 'appendCacheablePageType'
2148
],
2249
];
23-
}
50+
}
2451

25-
public function appendTransformations($context)
26-
{
52+
/**
53+
* Append API Framework page cache preferences
54+
*
55+
* @param array $context
56+
* delegate context
57+
*/
58+
public function appendPreferences($context)
59+
{
60+
// Create preference group
61+
$group = new XMLElement('fieldset');
62+
$group->setAttribute('class', 'settings');
63+
$group->appendChild(new XMLElement('legend', __('API Framework')));
2764

28-
// Add the @jsonForceArray transformation
29-
$context['transformer']->append(
30-
new Lib\Transformation(
31-
function (array $input, array $attributes=[]) {
32-
// First make sure there is an attributes array
33-
if (empty($attributes)) {
34-
return false;
35-
}
36-
// Only looking at the jsonForceArray property
37-
elseif (!isset($attributes['jsonForceArray']) || $attributes['jsonForceArray'] !== "true") {
38-
return false;
39-
}
40-
// This is already an indexed array.
41-
elseif (!Lib\array_is_assoc($input)) {
42-
return false;
43-
}
44-
// jsonForceArray is set, and it's true
45-
return true;
46-
},
47-
function (array $input, array $attributes=[]) {
48-
$result = [];
49-
// Encapsulate everything in an array
50-
foreach ($input as $key => $value) {
51-
$result[$key] = $value;
52-
unset($input[$key]);
53-
}
54-
$input[] = $result;
55-
return $input;
56-
}
57-
)
58-
);
59-
}
65+
// Append enable cache
66+
$label = Widget::Label();
67+
$input = Widget::Input('settings[api_framework][enable_caching]', self::CACHE_ENABLED, 'checkbox');
6068

61-
public function setJSONLauncher($context)
62-
{
63-
if ($_REQUEST['mode'] == 'administration') {
64-
return;
69+
if (self::isCacheEnabled()) {
70+
$input->setAttribute('checked', 'checked');
71+
}
72+
73+
$label->setValue($input->generate() . ' ' . __('Enable caching'));
74+
$group->appendChild($label);
75+
76+
// Append help
77+
$group->appendChild(new XMLElement('p', __('Rendered page content for pages with type \'cacheable\' will have their content stored and reused for subsequent loads.'), ['class' => 'help']));
78+
79+
// Cache lifetime
80+
$label = Widget::Label();
81+
$input = Widget::Input(
82+
'settings[api_framework][cache_lifetime]',
83+
self::getCacheLifetime(),
84+
null,
85+
['size' => '6']
86+
);
87+
$selected = self::getCacheDuration();
88+
$options = [
89+
[
90+
self::CACHE_DURATION_MINUTE,
91+
($selected == self::CACHE_DURATION_MINUTE),
92+
'minute(s)'
93+
],
94+
[
95+
self::CACHE_DURATION_HOUR,
96+
($selected == self::CACHE_DURATION_HOUR),
97+
'hour(s)'
98+
],
99+
[
100+
self::CACHE_DURATION_DAY,
101+
($selected == self::CACHE_DURATION_DAY),
102+
'day(s)'
103+
],
104+
[
105+
self::CACHE_DURATION_WEEK,
106+
($selected == self::CACHE_DURATION_WEEK),
107+
'week(s)'
108+
],
109+
];
110+
$select = Widget::Select('settings[api_framework][cache_duration]', $options, ['class' => 'inline', 'style' => 'display: inline; width: auto;']);
111+
112+
$label->setValue(__('Refresh page cache every %s %s', [$input->generate(false), $select->generate(false)]));
113+
$group->appendChild($label);
114+
115+
// Append help
116+
$group->appendChild(new XMLElement('p', __('Once page cache expires, the next time that page is loaded any existing cache data will be replaced.'), ['class' => 'help']));
117+
118+
// Append disable cleanup
119+
$label = Widget::Label();
120+
$input = Widget::Input('settings[api_framework][cache_disable_cleanup]', 'yes', 'checkbox');
121+
122+
if (!self::isCacheCleanupEnabled()) {
123+
$input->setAttribute('checked', 'checked');
124+
}
125+
126+
$label->setValue($input->generate() . ' ' . __('Disable Cleanup'));
127+
$group->appendChild($label);
128+
129+
// Append help
130+
$group->appendChild(new XMLElement('p', __('By default, any expired cache entries are automatically checked for and removed each time a cacheable page is rendered. If there the site has a large volume of cached content, you may wish to disable this to reduce load.'), ['class' => 'help']));
131+
132+
// Append new preference group
133+
$context['wrapper']->appendChild($group);
65134
}
66-
define('SYMPHONY_LAUNCHER', 'renderer_json');
67135

68-
include __DIR__ . '/src/Includes/JsonRendererLauncher.php';
136+
/**
137+
* Save preferences
138+
*
139+
* @param array $context
140+
* delegate context
141+
*/
142+
public function savePreferences($context)
143+
{
144+
if (!is_array($context['settings'])) {
145+
// Disable caching by default
146+
$context['settings'] = [
147+
'api_framework' => [
148+
'enable_caching' => self::CACHE_DISABLED,
149+
'cache_lifetime' => 1,
150+
'cache_duration' => self::CACHE_DURATION_HOUR,
151+
'cache_disable_cleanup' => 'no'
152+
]
153+
];
154+
} else {
155+
if (!isset($context['settings']['api_framework']['enable_caching'])) {
156+
// Disable caching if it has not been checked
157+
$context['settings']['api_framework']['enable_caching'] = self::CACHE_DISABLED;
158+
}
159+
160+
if (!isset($context['settings']['api_framework']['cache_disable_cleanup'])) {
161+
// Disable cache cheanup had been checked
162+
$context['settings']['api_framework']['cache_disable_cleanup'] = 'no';
163+
}
164+
165+
$context['settings']['api_framework']['cache_lifetime'] = max(1, (int)$context['settings']['api_framework']['cache_lifetime']);
166+
}
167+
}
168+
169+
/**
170+
* Check if cache is enabled
171+
*/
172+
public static function isCacheEnabled()
173+
{
174+
return
175+
strtolower(Symphony::Configuration()->get('enable_caching', 'api_framework')) == self::CACHE_ENABLED
176+
? true
177+
: false
178+
;
179+
}
180+
181+
/**
182+
* Check if cache cleanup is enabled
183+
*/
184+
public static function isCacheCleanupEnabled()
185+
{
186+
return
187+
strtolower(Symphony::Configuration()->get('cache_disable_cleanup', 'api_framework')) != 'yes'
188+
? true
189+
: false
190+
;
191+
}
192+
193+
/**
194+
* Convienence method for getting the cache_lifetime setting
195+
*/
196+
public static function getCacheLifetime()
197+
{
198+
return (int)Symphony::Configuration()->get('cache_lifetime', 'api_framework');
199+
}
200+
201+
/**
202+
* Convienence method for getting the cache_duration setting
203+
*/
204+
public static function getCacheDuration()
205+
{
206+
return Symphony::Configuration()->get('cache_duration', 'api_framework');
207+
}
208+
209+
/**
210+
* Takes the cache lifetime and duration and calculate when cache should
211+
* expire.
212+
* @returns timestamp
213+
*/
214+
public static function calculateNextCacheExpiryTime()
215+
{
216+
return strtotime(sprintf(
217+
"+%s %s",
218+
self::getCacheLifetime(),
219+
self::getCacheDuration()
220+
));
221+
}
222+
223+
/**
224+
* Converts cache lifetime and cache duration into seconds. This is used
225+
* when calculating the cache expiry time.
226+
* @returns integer
227+
*/
228+
public static function cacheLifetimeReal()
229+
{
230+
$value = self::getCacheLifetime();
231+
232+
switch (self::getCacheDuration()) {
233+
234+
case self::DURATION_WEEK:
235+
$value *= 7;
236+
237+
// no break
238+
case self::DURATION_DAY:
239+
$value *= 24;
240+
241+
// no break
242+
case self::DURATION_HOUR:
243+
$value *= 60;
244+
break;
245+
246+
}
247+
248+
return $value;
249+
}
250+
251+
/**
252+
* Append type for cacheable pages to page editor.
253+
*
254+
* @param array $context
255+
* delegate context
256+
*/
257+
public function appendCacheablePageType($context)
258+
{
259+
// Find page types
260+
$elements = $context['form']->getChildren();
261+
$fieldset = $elements[0]->getChildren();
262+
$group = $fieldset[2]->getChildren();
263+
$div = $group[1]->getChildren();
264+
$types = $div[2]->getChildren();
265+
266+
// Search for existing cacheable type
267+
$cacheableTypeAlreadyExists = false;
268+
foreach ($types as $type) {
269+
if ($type->getValue() == 'cacheable') {
270+
$cacheableTypeAlreadyExists = true;
271+
break;
272+
}
273+
}
274+
275+
// Append cacheable type
276+
if (!$cacheableTypeAlreadyExists) {
277+
$div[2]->appendChild(new XMLElement('li', 'cacheable'));
278+
}
279+
}
280+
281+
public function appendTransformations($context)
282+
{
283+
// Add the @jsonForceArray transformation
284+
$context['transformer']->append(new Lib\Transformation(
285+
function (array $input, array $attributes=[]) {
286+
// First make sure there is an attributes array
287+
if (empty($attributes)) {
288+
return false;
289+
}
290+
// Only looking at the jsonForceArray property
291+
elseif (!isset($attributes['jsonForceArray']) || $attributes['jsonForceArray'] !== "true") {
292+
return false;
293+
}
294+
// This is already an indexed array.
295+
elseif (!Lib\array_is_assoc($input)) {
296+
return false;
297+
}
298+
// jsonForceArray is set, and it's true
299+
return true;
300+
},
301+
function (array $input, array $attributes=[]) {
302+
$result = [];
303+
// Encapsulate everything in an array
304+
foreach ($input as $key => $value) {
305+
$result[$key] = $value;
306+
unset($input[$key]);
307+
}
308+
$input[] = $result;
309+
return $input;
310+
}
311+
));
312+
313+
// Add the @convertEmptyElementsToString transformation
314+
// Render empty string values instead of empty arrays
315+
// i.e. <banana></banana>, normally converted to
316+
// array(0) {} and thus banana: [], becomes banana: ""
317+
$context['transformer']->append(new Lib\Transformation(
318+
function (array $input, array $attributes=[]) {
319+
if (isset($attributes['convertEmptyElementsToString'])) {
320+
return true;
321+
}
322+
return false;
323+
},
324+
function (array $input, array $attributes=[]) {
325+
foreach ($input as $key => $value) {
326+
if (empty($value)) {
327+
$input[$key] = "";
328+
}
329+
}
330+
return $input;
331+
}
332+
));
333+
}
334+
335+
public function setJSONLauncher($context)
336+
{
337+
if ($_REQUEST['mode'] == 'administration') {
338+
return;
339+
}
340+
define('SYMPHONY_LAUNCHER', 'renderer_json');
341+
342+
include __DIR__ . '/src/Includes/JsonRendererLauncher.php';
343+
}
69344
}
70345
}

0 commit comments

Comments
 (0)