This should be a very simple question, but I'm finding it surprisingly hard to find an answer.
I'm creating custom pages using hook_menu that have both static content and dynamic aspects, (mainly from sql queries and views embeds etc...). My question is, how does drupal's cache system treat these custom pages?
I'm concerned because as the traffic ramps up on some occasions, I feel like I need some kind of caching control over the display of these pages, but at this point, I have no idea if they're automatically being cached, or if I need to somehow specify to drupal that I do indeed want them cached.
Just to clarify, these pages are not admin pages, but are accessible by anyone.
The result of a menu callback is cached only when the menu callback returns the output; when the menu callback prints the output, it's not cached.
The code execute to bootstrap Drupal when a page is requested is the following:
require_once './includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
$return = menu_execute_active_handler();
// Menu status constants are integers; page content is a string.
if (is_int($return)) {
switch ($return) {
case MENU_NOT_FOUND:
drupal_not_found();
break;
case MENU_ACCESS_DENIED:
drupal_access_denied();
break;
case MENU_SITE_OFFLINE:
drupal_site_offline();
break;
}
}
elseif (isset($return)) {
// Print any value (including an empty string) except NULL or undefined:
print theme('page', $return);
}
drupal_page_footer();
drupal_page_footer() is the function that caches the result.
function drupal_page_footer() {
if (variable_get('cache', CACHE_DISABLED) != CACHE_DISABLED) {
page_set_cache();
}
module_invoke_all('exit');
}
page_set_cache() is the function that does the real work.
function page_set_cache() {
global $user, $base_root;
if (!$user->uid && $_SERVER['REQUEST_METHOD'] == 'GET' && page_get_cache(TRUE)) {
// This will fail in some cases, see page_get_cache() for the explanation.
if ($data = ob_get_contents()) {
if (variable_get('page_compression', TRUE) && extension_loaded('zlib')) {
$data = gzencode($data, 9, FORCE_GZIP);
}
ob_end_flush();
cache_set($base_root . request_uri(), $data, 'cache_page', CACHE_TEMPORARY, drupal_get_headers());
}
}
}
The content is compressed (if the zlib is enabled), and saved in the cache.
If you want the cache the output of a custom menu callback, then you just need to return the output, instead of printing it directly.
function mymodule_callback() {
// …
return $output;
}
Instead of caching the output of the page, you can cache the data the module used to build its output. If, in example, the output data is obtained with an SQL query, you can cache the result of the query.
Related
I use if else for custom menus in Wordpress, to load various location menus based on parent page. The agency I work for is adding countless amounts of cities, and it's getting out of hand. One thing I am trying to do, is come up with a more efficient way to check the items, someone suggested switch, and I just wanted to throw this out there and see what you all think. These are not complete codes, and I know the menus are bad UX, and all that, it's not my call. I just want some input on performance differences. thanks.
Here is an example of switch code:
function is_subpage() {
global $post; // load details about this page
if ( is_page() && $post->post_parent ) { // test to see if the page has a parent
return $post->post_parent; // return the ID of the parent post
} else { // there is no parent so ...
return false; // ... the answer to the question is false
}
}
$selectedMenu = "primary";
$my_page_id = is_subpage();
if(!$my_page_id)
$my_page_id = get_the_ID();
switch ($my_page_id) {
case('489'):
$selectedMenu = 'columbus';
break;
case('6583'):
$selectedMenu = 'cumming';
break;
}
wp_nav_menu( array(
'theme_location' => 'main-menu',
'menu' => $selectedMenu,
'menu_class' => 'clearfix'
));
and here is an example of if else code:
if(is_page( '28' ) || '28' == $post->post_parent) { $locationMenu = 'louisville'; }
'menu' => $locationMenu,
Don't second guess or assume anything about the efficiency of an interpreter or compiler. if else might be better at one scenario and switch at another.
The problem with your code is readability and maintainability and not performance. It is hard to be specific without knowing all details about your needs, but it seems like what you need is to have at each post a custom field which indicates the menu associated with that post, and then the admin can configure them and you will have some more coffee time ;)
This is actually a worse solution in terms of performance, but if you really need the site to be fast then you are going to use a caching plugin which will make the whole php related performance discussion just a waste of time.
From a PHP perspective...
In lieu of having the page id to location table in a database, you could include a structure like this on pages you need it:
$idToLocation = array(
"489" => "columbus",
"6583" => "cumming"
// et cetera
);
Then to get the location:
$id = "489"; // for example
if (!array_key_exists($id, $idToLocation)) {
echo "location for id not found";
die();
}
$location = $idToLocation[$id];
I just upgrade MvcSiteMapProvider from v3 to v4.6.3.
I see the upgrade note indicate:
In general, any reference to System.Web.SiteMap.Provider will need to be updated to MvcSiteMapProvider.SiteMaps.Current
I am trying to get the sitemap node by using:
SiteMaps.Current.FindSiteMapNode(rawUrl)
But it always return null
I looked into the code. In the sitemap it's actually calling the function:
protected virtual ISiteMapNode FindSiteMapNodeFromUrlMatch(IUrlKey urlToMatch)
{
if (this.urlTable.ContainsKey(urlToMatch))
{
return this.urlTable[urlToMatch];
}
return null;
}
It's trying to find a match in the urlTable.
I am using Default implementation of XmlSiteMapProvider .
It define var url = node.GetAttributeValue("url");
siteMapNode.Url = url;
siteMapNode.UrlResolver = node.GetAttributeValue("urlResolver");
So if I did not define url or urlResolver attribute in the .sitemap file. These variables a set to empty string, when generate the node.
And when this nodes are passed to AddNode function in SiteMap.
When adding the node
bool isMvcUrl = string.IsNullOrEmpty(node.UnresolvedUrl) && this.UsesDefaultUrlResolver(node);
this code will check if there is url or urlResolver
// Only store URLs if they are clickable and are configured using the Url
// property or provided by a custom URL resolver.
if (!isMvcUrl && node.Clickable)
{
url = this.siteMapChildStateFactory.CreateUrlKey(node);
// Check for duplicates (including matching or empty host names).
if (this.urlTable
.Where(k => string.Equals(k.Key.RootRelativeUrl, url.RootRelativeUrl, StringComparison.OrdinalIgnoreCase))
.Where(k => string.IsNullOrEmpty(k.Key.HostName) || string.IsNullOrEmpty(url.HostName) || string.Equals(k.Key.HostName, url.HostName, StringComparison.OrdinalIgnoreCase))
.Count() > 0)
{
var absoluteUrl = this.urlPath.ResolveUrl(node.UnresolvedUrl, string.IsNullOrEmpty(node.Protocol) ? Uri.UriSchemeHttp : node.Protocol, node.HostName);
throw new InvalidOperationException(string.Format(Resources.Messages.MultipleNodesWithIdenticalUrl, absoluteUrl));
}
}
// Add the URL
if (url != null)
{
this.urlTable[url] = node;
}
Finally no url is add to the urlTable, which result in FindSiteMapNode cannot find anything.
I am not sure if there needs to be specific configuration. Or should I implement custom XmlSiteMapProvider just add the url.
ISiteMapNodeProvider instances cannot use the FindSiteMapNode function for 2 reasons. The first you have already discovered is that finding by URL can only be done if you set the url attribute explicitly in the node configuration. The second reason is that the SiteMapBuilder doesn't add any of the nodes to the SiteMap until all of the ISiteMapNodeProvider instances have completed running, so it would be moot to add the URL to the URL table anyway.
It might help if you explain what you are trying to accomplish.
The ISiteMapNodeProvider classes have complete control over the data that is added to the SiteMapNode instances and they also have access to their parent SiteMapNode instance. This is generally all that is needed in order to populate the data. Looking up another SiteMapNode from the SiteMap object while populating the data is not supported. But as long as the node you are interested in is populated in the same ISiteMapNodeProvider instance, you can just get a reference to it later by storing it in a variable.
Update
Okay, I reread your question and your comment and it now just seems like you are looking in the wrong place. MvcSiteMapProvider v4 is no longer based on Microsoft's SiteMap provider model, so using XmlSiteMapProvider doesn't make sense, as it would sidestep the entire implementation. The only case where this might make sense is if you have a hybrid ASP.NET and ASP.NET MVC application that you want to keep a consitant menu structure between. See Upgrading from v3 to v4.
There are 2 stages to working with the data. The first stage (the ISiteMapBuilder and ISiteMapNodeProvider) loads the data from various sources (XML, .NET attributes, DynamicNodeProviders, and custom implementations of ISiteMapNodeProvider) and adds it to an object graph that starts at the SiteMap object. Much like Microsoft's model, this data is stored in a shared cache and only loaded when the cache expires. This is the stage you have been focusing on and it definitely doesn't make sense to lookup nodes here.
The second stage is when an individual request is made to access the data. This is where looking up data based on a URL might make sense, but there is already a built-in CurrentNode property that finds the node matching the current URL (or more likely the current route since we are dealing with MVC) which in most cases is the best approach to finding a node. Each node has a ParentNode and ChildNodes properties that can be used to walk up or down the tree from there.
In this second stage, you can access the SiteMap data at any point after the Application_Start event such as within a controller action, in one of the built in HTML helpers, an HTML helper template in the /Views/Shared/DisplayTemplates/ directory, or a custom HTML helper. This is the point in the application life cycle which you might call the lines SiteMaps.Current.FindSiteMapNode(rawUrl) or (more likely) SiteMaps.Current.CurrentNode to get an instance of the node so you can inspect its Attributes property (the custom attributes).
public ActionResult About()
{
ViewBag.Message = "Your app description page.";
var currentNode = MvcSiteMapProvider.SiteMaps.Current.CurrentNode;
string permission = currentNode.Attributes.ContainsKey("permission") ? currentNode.Attributes["permission"].ToString() : string.Empty;
string programs = currentNode.Attributes.ContainsKey("programs") ? currentNode.Attributes["programs"].ToString() : string.Empty;
string agencies = currentNode.Attributes.ContainsKey("agencies") ? currentNode.Attributes["agencies"].ToString() : string.Empty;
// Do something with the custom attributes of the About page here
return View();
}
The most common usage of custom attributes is to use them from within a custom HTML helper template. Here is a custom version of the /Views/Shared/DisplayTemplates/SiteMapNodeModel.cshtml template that displays the custom attributes. Note that this template is called recursively by the Menu, SiteMapPath, and SiteMap HTML helpers. Have a look at this answer for more help if HTML helper customization is what you intend to do.
#model MvcSiteMapProvider.Web.Html.Models.SiteMapNodeModel
#using System.Web.Mvc.Html
#using MvcSiteMapProvider.Web.Html.Models
#if (Model.IsCurrentNode && Model.SourceMetadata["HtmlHelper"].ToString() != "MvcSiteMapProvider.Web.Html.MenuHelper") {
<text>#Model.Title</text>
} else if (Model.IsClickable) {
if (string.IsNullOrEmpty(Model.Description))
{
#Model.Title
}
else
{
#Model.Title
}
} else {
<text>#Model.Title</text>
}
#string permission = Model.Attributes.ContainsKey("permission") ? Model.Attributes["permission"].ToString() : string.Empty
#string programs = Model.Attributes.ContainsKey("programs") ? Model.Attributes["programs"].ToString() : string.Empty
#string agencies = Model.Attributes.ContainsKey("agencies") ? Model.Attributes["agencies"].ToString() : string.Empty
<div>#permission</div>
<div>#programs</div>
<div>#agencies</div>
How do you deal with the fact, that URLs are case sensitive in xPages even for parameters? For example URL:
my_page.xsp?folderid=785478 ... is not the same as ...
my_page.xsp?FOLDERID=785478
How to make, for example, a proper check that params contain some key e.g.
param.containsKey("folderid") which desnt work when there is 'FOLDERID' in URL.
I'd suggest defining a couple convenience #Functions:
var #HasParam = function(parameter) {
var result:boolean = false;
for (var eachParam : param.keySet()) {
if (eachParam.toLowerCase() == parameter.toLowerCase()) {
result = true;
break;
}
}
return result;
};
var #GetParam = function(parameter) {
var result = "";
if (#HasParam(parameter)) {
for (var eachParam : param.keySet()) {
if (eachParam.toLowerCase() == parameter.toLowerCase()) {
result = param.get(eachParam);
break;
}
}
}
return result;
};
Then you can safely query the parameters without caring about case. For bonus points, you could add requestScope caching so that you can skip looping through the keySet if you're examining a parameter that you've previously looked at during the same request.
you may use this function:
context.getUrlParameter('param_name')
then test if it's null or not.
make sure to decide for one,so either upper or lowercase
other than that i'd suggest something like
KeyValuePair<string,string> kvp = null;
foreach(KeyValuePair<string,string> p in param)
{
if(UPPERCASE(p.Key) == UPPERCASE("folderid"))
{
kvp = p;
break;
}
}
syntax isn't correct and idk the uppercase method in c# right now,but you get the point
The easiest answer is ofcourse the obvious. Be sure that the parameters you are using througout your application are always the same on every url you are generating and know what to expect. A good approach to accomplish this is to create a ssjs function which generates url's for you according to the objects you submit.
In this function you could check which object you are receiving and with the use of keywords and so forth generate the correct url. This way generating twice a url with the same input parameters should always generate the exact same url.
another option would be just to double check with a bit of code like this
var key = "yourkey";
if(param.contains(#uppercase(key)) || param.contains(#lowercase(key)){
// do stuff
}
But should not be necesarry if the url you are parsing is generated by your own application
Edit after post of topic starter
Another option would be to grap the url directly from from the facescontext and to convert it to a string first. When it is a string you can parse the parameters yourself.
You can combine server side substitution/redirection to get around the issue that David mentioned. So a substitution rule will redirect incoming patern like this:
http://myhost/mypage/param (/mypage/* => which converts to - /dbpath/mypage.xsp?*) - substitution is tricky so please handle with care.
Also I believe I read somewhere that context.getUrlParameter is not case sensitive - can someone please confirm this.
Hope this helps.
I'm looking for an example of how to run a custom page template for a single url in drupal 6, would be nice to have a preprocess function too.
I used the following code in a recent Drupal 6 project; it requires entries into the template.php file which resides in the root of your theme folder. Simply drop the template page you create into the root of your theme folder, and you're off. {:¬)
You may have this function specified in your template.php file already; in which case, you'd probably have to refactor to add this. Here's the function in full:
function yourThemeName_preprocess_page(&$vars) {
if (isset($vars['node'])) {
$node = $vars['node'];
$vars['template_files'] = array();
switch ($node->nid) {
case '17': /* to override a specific node ID */
$vars['template_files'][] = 'page-my-page-name';
break;
default: /* to override a content type */
switch ($node->type) {
case 'page':
$vars['template_files'][] = 'page-normal-page';
break;
case 'my_own_content_type':
$vars['template_files'][] = 'page-my-own-content-type';
break;
default:
/* take no action */
}
}
}
}
Where I've specified 'page-my-page-name', note that Drupal (or rather, PHPTemplate) will add the '.tpl.php' part automatically.
This enables you to override by node ID first (more specific), and then more generally by content type, e.g. story or page. To add more overrides, just add more cases in the right place.
Hope this helps.
Humm, you might have to use the Module Panels3 and create a landing page. You can override every element of that page (even outside content area)
What I am looking for is a page_id/view_id that I can use to identify and style specific pages. I would use the title or the url, but there is a chance that it could change if the a higher-up decides that the page should no longer be called Golf, but rather Tee-Time because he likes it better.
Presumably this identifier would not change if the current page were to be a paged view (page 1,2,3,4...).
One way of solving this is the following. It's depending on the url, so if it changes, so does the class-name.
In my themes template.php I implemented hook_preprocess_page:
function mytheme_preprocess_page(&$vars, $hook) {
$body_classes = array();
$body_classes[] = 'page-' . _get_page_name($_SERVER['REQUEST_URI']);
$vars['body_classes'] = implode(' ', $body_classes);
}
function _get_page_name($request_uri) {
static $numeric_subsection = array(
'/node/' => 'node',
);
$preAlias = $request_uri;
$alias = substr(strrchr($preAlias, "/"), 1);
if (strpos($alias, '?') > -1) {
$alias = substr($alias, 0, strpos($alias, '?'));
}
$page_name = $alias;
if (empty($alias)) {
$page_name = 'start';
}
else if (is_numeric($alias)) {
foreach ($numeric_subsection as $section => $pn) {
if (strpos($preAlias, $section) > -1) {
$page_name = $pn;
}
}
}
return $page_name;
}
Then in the main page-template:
<body class="<?php print $body_classes; ?>">
This isn't a generic solution. So you'll probably have to customize this for your specific needs. It will for example need som tweaking to play nicely with path auto.
This depends a little on how your site is put together (panel pages, view pages, "normal" pages). Essentially, you need to figure out what vars are in scope, and then determine which information in them can be used. To determine what is in scope, you can use print_r(array_keys(get_defined_vars())); and then poke around in the individual vars.
An option is to do something in theme_preprocess_page. One option is to get the page data via page_manager_get_current_page(), poke around in there, and then add body classes as needed. Without knowing what you are doing, you essentially need to print_r the results somewhere, look at what you have, and go from there.