The more I think about this, the more evident it becomes that the acceptance, verification and creation of multi-language friendly alias paths is going to be quite involved. I suggest that to start with we only support language dependent friendly aliases, with no paths. If we can get that working, then the logic required to generalise it later is unlikely to break what we have already done.
$lang = $yams->GetCurrentLangId();
At this stage, we wont know what the current language is. This is currently decided after the id of the document being requested had been determined by MODx. It’s normally necessary to know the id of the document first so that we can decide whether it is a multilingual document or not - and if it is, we can attempt to figure out the language from the URL.
$docId = $this->getDocumentIdentifier('alias');
Better call it $alias or something like that. The OnPageNotFound event doesn’t get passed anything from MODx as far as I can see, so we’ll get the alias from $_GET[’q’]. If it’s got any forward slashes in it then we can just return since we wont initially be supporting friendly alias paths.


<?php
require_once( dirname( __FILE__ ) . '/class/yams-ux.class.inc.php' );
$evt = &$modx->Event;
switch ($evt->name) {
case 'OnPageNotFound':
$yams_ux = YAMS_UX::getInstance();
$yams_ux->onPageNotFound();
}
?><?php
require_once( dirname( __FILE__ ) . '/yams.class.inc.php' );
if (!class_exists('YAMS_UX')) {
class YAMS_UX {
private static $instance;
private $modx;
private $lang;
public static function getInstance() {
if (!isset( self::$instance)) {
$clazz = __CLASS__;
self::$instance = new $clazz();
}
return self::$instance;
}
private function __construct() {
global $modx;
$this->modx = &$modx;
$yams = YAMS::GetInstance();
$this->lang = $yams->GetCurrentLangId();
}
public function onPageNotFound() {
// mgb: only process if we're in 'alias' mode
if ($this->modx->documentMethod === 'alias') {
// mgb: build the 'path' to the resource
$alias_path = array();
if ($this->modx->virtualDir != '') {
$virtualDirPaths = explode('/', $this->modx->virtualDir);
foreach ($virtualDirPaths as $virtualDirPath) {
array_push($alias_path, $virtualDirPath);
}
}
array_push($alias_path, $this->modx->documentIdentifier);
$docId = $this->getDocumentIdentifier($alias_path);
if ($docId !== NULL) {
$this->modx->sendForward($docId);
exit();
}
}
}
public function getDocumentIdentifier($alias_path, $parent = 0) {
$docId = NULL;
$alias = array_shift($alias_path);
$sc = $this->modx->getFullTableName('site_content');
$tv = $this->modx->getFullTableName('site_tmplvars');
$tvc = $this->modx->getFullTableName('site_tmplvar_contentvalues');
$sql = "SELECT sc.id
FROM $sc sc, $tv tv, $tvc tvc
WHERE sc.id = tvc.contentid
AND sc.parent = $parent
AND tv.id = tvc.tmplvarid
AND tv.name = 'alias_{$this->lang}'
AND tvc.value = '$alias'";
$result = $this->modx->db->query($sql);
$count = $this->modx->recordCount($result);
// mgb: is there a TV match for "alias_${_GET['yams_lang']}" == "${_GET['q']}" ?
if ($count == 1) {
$row = $this->modx->fetchRow($result);
$docId = $row['id'];
}
// mgb: do we need to recurse?
if ($docId !== NULL && !empty($alias_path)) {
// mgb: if the child resource is not found should we
// forward to the parent resource or default back
// to the start page?
$docId = $this->getDocumentIdentifier($alias_path, $docId);
}
return $docId;
}
}
}
?>
// If this is a valid multilingual request,
// the current language group id will be set.
$isValidMultilingualRequest = $yams->IsValidMultilingualRequest( $langId )
if ( ! $isValidMultilingualRequest )
{
return;
}
$isMultilingualDocument = $yams->IsMultilingualDocument( $docId );
<?php
require_once( dirname( __FILE__ ) . '/yams.class.inc.php' );
if (!class_exists('YAMS_UX')) {
class YAMS_UX {
private static $instance;
private $modx;
private $yams;
private $lang;
public static function getInstance() {
if (!isset( self::$instance)) {
$clazz = __CLASS__;
self::$instance = new $clazz();
}
return self::$instance;
}
private function __construct() {
global $modx;
$this->modx = &$modx;
$this->yams = YAMS::GetInstance();
}
public function onPageNotFound() {
if (!$this->isValidYamsRequest()) return;
$path = $this->createDocumentPath();
$docId = $this->findDocumentIdentifier($path);
if ($docId !== NULL) {
$this->modx->sendForward($docId);
exit();
}
}
private function isValidYamsRequest() {
$isValidMultilingualRequest = $this->yams->IsValidMultilingualRequest($this->lang);
return $isValidMultilingualRequest && ($this->modx->documentMethod === 'alias');
}
private function createDocumentPath() {
$q = (strlen($this->modx->virtualDir) > 0 ? $this->modx->virtualDir . '/' : '') . $this->modx->documentIdentifier;
$path = explode('/', $q);
return $path;
}
private function findDocumentIdentifier(array & $path) {
$docId = 0;
foreach ($path as $alias) {
if ($docId === NULL) {
break;
}
$docId = $this->lookupDocumentIdentifier($alias, $docId);
}
return $docId;
}
private function lookupDocumentIdentifier($alias, $parent = 0) {
$docId = $this->queryDocumentIdentifierByMultilingualAlias($alias, $parent);
if ($docId === NULL) {
$docId = $this->queryDocumentIdentifierByResourceAlias($alias, $parent);
}
return $docId;
}
private function queryDocumentIdentifierByMultilingualAlias($alias, $parent = 0) {
$sc = $this->modx->getFullTableName('site_content');
$tv = $this->modx->getFullTableName('site_tmplvars');
$tvc = $this->modx->getFullTableName('site_tmplvar_contentvalues');
$sql = "SELECT sc.id
FROM $sc sc, $tv tv, $tvc tvc
WHERE sc.id = tvc.contentid
AND sc.parent = $parent
AND tv.id = tvc.tmplvarid
AND tv.name = 'alias_{$this->lang}'
AND tvc.value = '$alias'";
return $this->queryDocumentIdentifier($sql);
}
private function queryDocumentIdentifierByResourceAlias($alias, $parent = 0) {
$sc = $this->modx->getFullTableName('site_content');
$sql = "SELECT sc.id
FROM $sc sc
WHERE sc.alias = '$alias'
AND sc.parent = $parent";
return $this->queryDocumentIdentifier($sql);
}
private function queryDocumentIdentifier($sql) {
$docId = NULL;
$result = $this->modx->db->query($sql);
$count = $this->modx->recordCount($result);
if ($count == 1) {
$row = $this->modx->fetchRow($result);
$docId = $row['id'];
}
return $docId;
}
}
}
?>
One thing that’s missing is consideration of the fact that the parent documents might be multilingual or monolingual. If they are monolingual then you have to get the alias from the standard document variable field, rather than from a template variable field.
You can use
$isMultilingualDocument = $yams->IsMultilingualDocument( $docId );
to determine what type it is. (This will involve doing a database query to look up the template id for the given document and check it against the list of active multilingual templates maintained by YAMS.)
We also need to think about what happens if any of the documents are unpublished and make sure that what we do is consistent with what happens with normal FURLs.
<?php
/**
* The YAMS_UX (User Extensions) plugin code.
*
* @author mgbowman (http://modxcms.com/forums/index.php?action=profile;u=21916)
* @copyright Matthew Bowman 2009
* @license GPL v3
* @version 0.0.4
*/
require_once( dirname( __FILE__ ) . '/class/yams-ux.class.inc.php' );
$yams_ux = YAMS_UX::getInstance();
if ($yams_ux === NULL) {
return;
}
$evt = &$modx->Event;
switch ($evt->name) {
case 'OnPageNotFound':
if ($modx->documentMethod === 'alias') {
$q = (strlen($modx->virtualDir) > 0 ? $modx->virtualDir . '/' : '') . $modx->documentIdentifier;
$docId = $yams_ux->getDocumentIdentifier($q);
if ($docId !== NULL) {
$modx->sendForward($docId);
exit();
}
}
}
?><?php
/**
* The main YAMS_UX (User Extensions) service - currently manages
* multilingual FURL routing.
*
* @author mgbowman (http://modxcms.com/forums/index.php?action=profile;u=21916)
* @copyright Matthew Bowman 2009
* @license GPL v3
* @version 0.0.4
*/
require_once( dirname( __FILE__ ) . '/yams.class.inc.php' );
if (!class_exists('YAMS_UX')) {
class YAMS_UX {
private static $instance = NULL;
private $modx;
private $yams;
private $lang;
public static function getInstance() {
if (self::$instance === NULL) {
$yams = YAMS::GetInstance();
$isMulti = $yams->IsValidMultilingualRequest($lang);
if ($isMulti) {
$clazz = __CLASS__;
self::$instance = new $clazz($lang);
}
}
return self::$instance;
}
private function __construct($lang) {
global $modx;
$this->modx = &$modx;
$this->yams = YAMS::GetInstance();
$this->lang = $lang;
}
public function getDocumentIdentifier($q) {
$sc = $this->modx->getFullTableName('site_content');
$tv = $this->modx->getFullTableName('site_tmplvars');
$tvc = $this->modx->getFullTableName('site_tmplvar_contentvalues');
$tmpl = "SELECT sc.id
FROM $sc sc, $tv tv, $tvc tvc
WHERE sc.id = tvc.contentid
AND sc.parent IN ({parent})
AND tv.id = tvc.tmplvarid
AND tv.name='alias_{$this->lang}'
AND tvc.value='{alias}'
UNION
SELECT sc.id
FROM $sc sc
WHERE sc.alias='{alias}'";
$aliases = explode('/', $q);
$sql = '0';
while (!empty($aliases)) {
$alias = array_shift($aliases);
$sql = str_replace('{alias}', $alias, str_replace('{parent}', $sql, $tmpl));
};
$docId = NULL;
if ($sql !== '0') {
$result = $this->modx->db->query($sql);
$count = $this->modx->recordCount($result);
if ($count == 1) {
$row = $this->modx->fetchRow($result);
$docId = $row['id'];
}
}
return $docId;
}
public function getDocumentAlias($docId) {
$sc = $this->modx->getFullTableName('site_content');
$tv = $this->modx->getFullTableName('site_tmplvars');
$tvc = $this->modx->getFullTableName('site_tmplvar_contentvalues');
$tmpls[TRUE] = "SELECT sc.parent, tvc.value AS alias
FROM $sc sc, $tv tv, $tvc tvc
WHERE sc.id = {docId}
AND sc.id = tvc.contentid
AND tv.id = tvc.tmplvarid
AND tv.name = 'alias_{$this->lang}'";
$tmpls[FALSE] = "SELECT sc.parent, sc.alias
FROM $sc sc
WHERE sc.id = {docId}";
$path = array();
do {
$isMultilingual = $this->yams->IsMultilingualDocument($docId);
$sql = str_replace('{docId}', $docId, $tmpls[$isMultilingual]);
$result = $this->modx->db->query($sql);
$count = $this->modx->recordCount($result);
if ($count == 1) {
$row = $this->modx->fetchRow($result);
$path[] = $row['alias'];
$docId = $row['parent'];
} else {
$docId = 0;
}
} while ($docId != 0);
// mgb: don't forget the language
$path[] = $this->lang;
$alias = implode('/', array_reverse($path));
return $alias;
}
}
}
?>
$modifier = $yams->GetEncodingModifier();
explode('/', $q)
// becomes
preg_split( '/\//' . $modifier, $q );
// and
str_replace('{parent}', $sql, $tmpl)
// becomes
preg_replace('/\{parent\}/' . $modifier, $sql, $tmpl)
$path[] = $this->lang;
Regarding going from alias -> id, I decided to just recursively build an SQL statement which had sub-select statements for matching on the parent id. This way you should end with either no id (malformed URL) or exactly 1 id. If there were 2 or more, wouldn’t this mean multiple resources were accessible using the same path?.In answer to your question - I can’t see how this would ever occur since each document only has one physical parent - oh, unless perhaps someone had specified the same alias on multiple documents by mistake. That could happen I guess.
$requiredAlias = $this->getDocumentAlias( $docId )
if ( $requiredAlias != $q )
{
return NULL;
}
else
{
return $docId;
}
if ( $this->modx->config['friendly_alias_urls'] == 1)
{
// the existing code
}
else
{
// only get the 'basename'
}
<?php
/**
* The YAMS_UX (User Extensions) plugin code.
*
* @author mgbowman (http://modxcms.com/forums/index.php?action=profile;u=21916)
* @copyright Matthew Bowman 2009
* @license GPL v3
* @version 0.0.5
*/
require_once( dirname( __FILE__ ) . '/class/yams-ux.class.inc.php' );
$evt = &$modx->Event;
switch ($evt->name) {
case 'OnPageNotFound':
if ($modx->documentMethod === 'alias') {
$q = (strlen($modx->virtualDir) > 0 ? $modx->virtualDir . '/' : '') . $modx->documentIdentifier;
$yams_ux = YAMS_UX::GetInstance();
$docId = $yams_ux->GetDocumentIdentifier($q);
if ($docId !== NULL) {
$modx->sendForward($docId);
exit();
}
}
}
?><?php
/**
* The main YAMS_UX (User Extensions) service - currently manages
* multilingual FURL routing.
*
* @author mgbowman (http://modxcms.com/forums/index.php?action=profile;u=21916)
* @copyright Matthew Bowman 2009
* @license GPL v3
* @version 0.0.5
*/
require_once( dirname( __FILE__ ) . '/yams.class.inc.php' );
if (!class_exists('YAMS_UX'))
{
class YAMS_UX
{
private function BuildGenericIdentifierQuery($alias)
{
return
'SELECT DISTINCT content.id
FROM ('
. $this->BuildMonolingualIdentifierQuery($alias) .
' UNION '
. $this->BuildMultilingualIdentifierQuery($alias) .
') content';
}
private function BuildMonolingualIdentifierQuery($alias)
{
$sc = $this->itsMODx->getFullTableName('site_content');
$lang = $this->GetCurrentLangId();
$alias = $this->itsMODx->db->escape($alias);
return
"SELECT sc.id
FROM $sc sc
WHERE sc.alias = '$alias'";
}
private function BuildMultilingualIdentifierQuery($alias)
{
$sc = $this->itsMODx->getFullTableName('site_content');
$st = $this->itsMODx->getFullTableName('site_tmplvars');
$stc = $this->itsMODx->getFullTableName('site_tmplvar_contentvalues');
$lang = $this->GetCurrentLangId();
$alias = $this->itsMODx->db->escape($alias);
return
"SELECT sc.id
FROM $sc sc, $st st, $stc stc
WHERE sc.id = stc.contentid
AND st.id = stc.tmplvarid
AND st.name = 'alias_$lang'
AND stc.value = '$alias'";
}
private function BuildMonolingualAliasQuery($docId)
{
$sc = $this->itsMODx->getFullTableName('site_content');
return
"SELECT sc.parent, sc.alias
FROM $sc sc
WHERE sc.id = $docId";
}
private function BuildMultilingualAliasQuery($docId)
{
$sc = $this->itsMODx->getFullTableName('site_content');
$st = $this->itsMODx->getFullTableName('site_tmplvars');
$stc = $this->itsMODx->getFullTableName('site_tmplvar_contentvalues');
$lang = $this->GetCurrentLangId();
return
"SELECT sc.parent, stc.value AS alias
FROM $sc sc, $st st, $stc stc
WHERE sc.id = $docId
AND sc.id = stc.contentid
AND st.id = stc.tmplvarid
AND st.name = 'alias_$lang'";
}
public function GetDocumentIdentifier($q)
{
$docId = NULL;
$modifier = $this->GetEncodingModifier();
// mgb: split $q on '/' to create the 'path' the the resource
$path = preg_split('/\//' . $modifier, $q);
// mgb: grab the 'target' alias from the end of the path (and escape it)
$alias = array_pop($path);
$sql = $this->BuildGenericIdentifierQuery($alias);
$result = $this->itsMODx->db->query($sql);
$count = $this->itsMODx->recordCount($result);
// mgb: loop through all docIds looking up the 'target' alias
// and comparing $q to the full alias of the docId
while ($count > 0)
{
$row = $this->itsMODx->fetchRow($result);
$id = $row['id'];
$target = $this->GetDocumentAlias($id);
if (strcmp($alias, $target) == 0)
{
$docId = $id;
break;
}
$count--;
}
return $docId;
}
public function GetDocumentAlias($docId) {
$path = array();
do {
$isMultilingual = $this->IsMultilingualDocument($docId);
$sql = ($isMultilingual ? $this->BuildMultilingualAliasQuery($docId) : $this->BuildMonolingualAliasQuery($docId));
$result = $this->itsMODx->db->query($sql);
$count = $this->itsMODx->recordCount($result);
if ($count == 1) {
$row = $this->itsMODx->fetchRow($result);
$path[] = $row['alias'];
$docId = $row['parent'];
} else {
$docId = 0;
}
} while ($docId != 0);
$alias = implode('/', array_reverse($path));
return $alias;
}
/*
* YAMS core migration helpers.
*
* When migrating the above code into YAMS core,
* we can simply leave the below variables & functions out
* (they are already in YAMS core and are the same).
*/
private static $theirInstance;
private $itsMODx;
public static function GetInstance()
{
if (!isset(self::$theirInstance))
{
$c = __CLASS__;
self::$theirInstance = new $c();
}
return self::$theirInstance;
}
private function __construct()
{
$this->Initialise();
}
private function Initialise()
{
global $modx;
$this->itsMODx = &$modx;
}
private function GetEncodingModifier()
{
$yams = YAMS::GetInstance();
return $yams->GetEncodingModifier();
}
private function GetCurrentLangId()
{
$yams = YAMS::GetInstance();
return $yams->GetCurrentLangId();
}
public function IsMultilingualDocument($docId = NULL, $template = NULL)
{
$yams = YAMS::GetInstance();
return $yams->IsMultilingualDocument($docId, $template);
}
}
}
?>