I was playing around with my UserUrls plugin to make it more flexible, and it turned into a very flexible friendly URL system. It’s pretty cool - in my current project, I have it generating 10 different friendly URL "schemas" at once (users, users/topics, 3 location types, location/ topics, just topics, and posts), covering 6 database tables, with currently 27 configurable settings for each URL schema.
The code is on github:
https://github.com/yoleg/CustomUrls
It probably won’t be in the repository very soon, since the settings are currently stored in two complex JSON arrays (defaults and URL schemas), which are not very user friendly and need a CMP. I made an install script to generate the JSON arrays from two PHP arrays. In other words, it is ready to integrate into another component, but not quite for use by itself yet.
Here’s what the plugin does:
1. On OnPageNotFound, it loads CustomUrls as a service, parses the URL, saves the resulting schema object, sets a couple of REQUEST parameters, and shows the appropriate landing page.
2. On OnLoadWebDocument, it checks if the page was accessed directly or through CustomUrls (and optionally redirects if accessed directly), retrieves and validates the stored URL schema, and optionally sets MODx placeholders from the database object(s) matching the schema.
3. On OnWebPagePrerender, it str_replaces the current page’s URL with the current CustomUrl. This perfectly solves the problem of having to fix canonicals, breadcrumbs, and any other links to the current resource without interfering with GET parameters. You can also pass your own array of additional search and replace strings for each schema. I am purposefully staying away from any regular expressions, to keep everything efficient.
URL structure:
base_url.prefix.alias.suffix.delimiter.action-OR-child-schema
Examples:
- site.com/pre/fix/USERNAME.suffix (prefix = "pre/fix/", suffix = ".suffix", delimiter="/")
- site.com/prefix-USERNAME-suffix/action (prefix = "prefix-", suffix = "-suffix", delimiter="/", action-map = {"action":"#"})
- site.com/pre/fix/USERNAME/category/pre/fix/CATEGORY/category-action (example using child schema)
Other stuff:
The CustomUrls service is designed to integrate with other components, and includes "makeUrl" methods. "MakeUrl" and "GetInfo" or similar snippets are planned but not yet implemented. Finally, although you can "chain" multiple schemas with multiple tables, there is no support for joining tables - I recommend you generate your own "alias" columns for any tables that need to be joined. I will probably be including methods and example code to help with alias generation.
Usage:
With multiple schemas, there is a huge potential for conflicts. I recommend you use required prefixes and suffixes to quickly exclude schemas, validate your aliases to not conflict with each other, and carefully plan the order of the schemas (which are executed in the order they appear in the setting’s JSON array). Once a matching schema is found, no further schemas are checked except as "child schemas" of the matching schema.
If using URL replacement, change the alias of the landing page to something complex and unique to avoid accidentally replacing legitimate text.
If using placeholders, a "display" placeholder is provided to allow you to use a single placeholder anywhere - just stick it in the landing page’s "pagetitle" field like so: [[!+customurls.display]]. Tweak the settings to choose which field is used for display and which prefix it uses.
The settings:
Each "URL schema" is a representation of an array of settings. The 27 default settings are loaded first, then overridden by your custom default settings, then overridden by the settings for each particular schema. Here are the defaults and the custructor for the CustomUrls service:
Code updated July 19, 2011 - check GitHub for newest version.
<?php
class customUrls {
// ...
public $defaults = array(
'landing_resource_id' => 0, // the resource id to redirect to
'set_request' => true, // If true, sets $_REQUEST parameters
'request_prefix' => 'user_', // $_REQUEST parameter prefix
'request_name_id' => 'id', // $_REQUEST parameter for the value of the search_result_field
'request_name_action' => 'action', // $_REQUEST parameter for action (if found in the action map)
'set_get' => true, // If true, sets $_GET parameters (using same settings as $_REQUEST)
'base_url' => '', // NOT the system base_url, just a prefix to add to all generated URLs
'lowercase_url' => true, // Generates lowercase urls. Does not affect searches.
'url_prefix' => '', // A prefix to append to the start of all urls
'url_prefix_required' => true, // Resolve the URL without the prefix?
'url_suffix' => '', // A suffix to append to the end of all urls
'url_suffix_required' => true, // Resolve the URL without the suffix?
'url_delimiter' => '/', // The separator between the main URL and the action
'load_modx_service' => array(), // loads as service if not empty. Must have the following keys set: 'name', 'class', 'package' OR 'path', and 'config' (config is an array). If you specify a lowercase package name, the path will be generated automatically for you based on either package.core_path setting or the default component structure.
'search_class' => 'modUser', // the xPDOObject class for the database table to search through
'search_field' => 'username', // the field to use in the URL
'search_result_field' => 'id', // the field to pass to the resource via the request_name_id
'search_display_field' => '', // the field to set in a special "display" placeholder to allow you to use the same placeholder for multiple schemas (defaults to search field)
'search_where' => array('active' => 1), // an additional filter for the database query (xpdo where)
'search_class_test_method' => '', // a method name of the class to run. If resolves to false, will not continue. Useful for permissions checking.
'action_map' => array(), // an array of keys (action names) and values (resource ids) to use for the sub-actions
'redirect_if_accessed_directly' => true,// will redirect to the error page if visited without CustomUrls
'redirect_if_object_not_found' => true, // will redirect to the error page if the object is not found
'set_placeholders' => true, // will generate some placeholders on the page storing the object field values
'placeholder_prefix' => 'customurls', // the placeholder prefix to use if set_placeholders is true
'display_placeholder' => 'display', // the placeholder for the display value to use if set_placeholders is true
'custom_search_replace' => array(), // an array of search => replace pairs to str_replace the output with
'run_without_parent' => true, // if set to false, will not be run unless called by another schema in the child_schemas array
'child_schemas' => array(), // an array of schema_names OR schema_name => (array) overrides to run the URL remainder through
'url_from_params' => true, // if the page is accessed directly but with the proper GET parameters, the plugin will try to detect the schema from the GET or REQUEST params and forward to the friendly Url. Useful for Quip and similar components that redirect directly to the current page afterwards.
'strict' => false, // "strict mode" if set to true, if any part of the URL is left over and not parsed, will treat the match as failed
'children_inherit_landing' => false, // if true, children schemas will use the landing page of the parent
);
//...
function __construct(modX &$modx,array $config = array()) {
$this->modx =& $modx;
$this->config = $config;
$defaults = $this->defaults;
$custom_defaults = $this->modx->fromJSON($this->modx->getOption('customurls.defaults',null,'[]'));
$defaults = array_merge($defaults,$custom_defaults);
$this->defaults = $defaults; // override stored defaults with new defaults
$url_schemas = $this->modx->fromJSON($this->modx->getOption('customurls.schemas',null,'{"users":{"request_prefix":"uu_","request_name_id":"userid"}}'));
$this->unprocessed = $url_schemas;
// clean and register url schemas
foreach ($url_schemas as $schema_name => $config) {
// merge config with defaults
$config = array_merge($defaults,$config);
// register landing page in resource map
$landing = $config['landing_resource_id'];
if (empty($landing)) continue;
$this->addLanding($landing,$schema_name);
$config['key'] = $schema_name;
// skip child-only schemas
$top_level = $config['run_without_parent'];
if (!$top_level) continue;
// save the object
// constructor also loads children into $schema->children and registers child pages
$schema = new cuSchema($this,$config);
$this->schemas[$schema_name] = $schema;
}
}
//...
Troubleshooting:
Call the following snippet on the landing pages to see the placeholders and request parameters set:
<?php
/**
* Outputs debugging info for MODx revolution
* Author: Oleg Pryadko ([email protected])
* License: public domain
*/
$output = '';
// not used - for syntax checking only
global $modx;
if (false) $modx = new modX('');
// Display Errors
error_reporting(E_ALL); ini_set('display_errors',true);
$modx->setLogTarget('HTML');
$modx->setLogLevel(modx::LOG_LEVEL_WARN);
// display non-system placeholders
$output_array = array();
$array = $modx->placeholders;
foreach ($array as $key => $value) {
if (strpos($key,'+') !== 0) $output_array[$key] = $value;
}
$output .= '<pre>';
$output .= '<br /><br />Placeholders: '.htmlentities(print_r($output_array,1));
// display global arrays
if (!empty($_REQUEST)) {
$output .= '<br /><br />REQUEST: '.htmlentities(print_r($_REQUEST,1));
}
if (!empty($_POST)) {
$output .= '<br /><br />POST: '.htmlentities(print_r($_POST,1));
}
if (!empty($_GET)) {
$output .= '<br /><br />GET: '.htmlentities(print_r($_REQUEST,1));
}
$output .= '</pre>';
return $output;