We launched new forums in March 2019—join us there. In a hurry for help with your website? Get Help Now!
    • 19209
    • 18 Posts
    Quote from: PMS at Aug 06, 2009, 01:50 PM

    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.

    I agree. The more I thought about it the more "use cases" became evident (i.e. FURL prefix and/or suffix).

    The below is constrained by the following assumptions:

    • YAMS uses only "Root Name" mode
    • No paths, just top-level aliases

    Quote from: PMS at Aug 06, 2009, 01:50 PM

    $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.

    If YAMS is configured for "Root Name" mode (i.e. http://localhost/en/news vs. http://localhost/fr/nouvelles), then shouldn’t GetCurrentLangId() return the current language from the URL without needing to know whether the document is multilingual or not? Using FURL (mod_rewrite) then http://localhot/en/news becomes http://localhost?q=news&yams_lang=en - basically the request is for the resource where alias_en=’news’.

    The "OnPageNotFound" event is fired as a result of the standard MODx FURL processing not finding a resource with the current alias. Therefore, if $_GET[’q’] is set (and is an alias) and $_GET[’yams_lang’] is set then we’re looking for the document that has the TV "alias_${_GET[’yams_lang’]}" = "${_GET[’q’]}"

    If querying the database returns a result, then you know you found the correct multilingual document (unless for some odd reason there’s a non-multilingual document with TVs for alias_{lang})

    If someone tried to access http://localhost/fr/news - why would you try to find all variants of alias_{lang} when you know the target is alias_fr=’news’? (which shouldn’t be found ... there’s no resource with a french alias of ’news’ so you should default to the MODx way of handling this ... show the French start page).

    Quote from: PMS at Aug 06, 2009, 01:50 PM

    $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.

    Yeah, this was a late night rough proof-of-concept. I’m going to abstract it today and add hooks for all the UCs I can think of for future functionality.

    I’ll report back with today’s progress - as I said I absolutely need this so I’m 100% committed to adding this functionality to YAMS laugh

    Thanks again for all the help,
    --mgbowman
      • 19209
      • 18 Posts
      Here’s rough draft #2 which supports nested paths (man I’m a sucker for recursion) tongue

      assets/modules/yams/yams-ux.plugin.inc.php
      <?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();
      }
      
      ?>


      assets/modules/yams/class/yams-ux.class.inc.php
      <?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;
      		}
      
      	}
      
      }
      
      ?>
        • 22851
        • 805 Posts
        There’s good reason why, with language ’independent’ aliases, you would start with the id and then determine which language should be displayed. However, I think you’re right about fixing the language up front in this case. If we get to the OnPageNotFound event, then either it was a badly formed URL or someone is trying to access a multilingual document via a language dependent alias, which, because of the way I have set it up, will always allow the requested language to be determined from the format of the URL, no matter what alias or combination of YAMS modes (root, server, query) are active.

        Please use
        // If this is a valid multilingual request,
        // the current language group id will be set.
        $isValidMultilingualRequest = $yams->IsValidMultilingualRequest( $langId )
        if ( ! $isValidMultilingualRequest )
        {
          return;
        }
        

        to get the language group id. Please note however, that this is currently a private method. I’ll make this and IsValidMonolingualRequest public for the next release. (That’s likely to be later today, since I have fixed some bugs.) In the meantime, you’ll have to make the edit yourself in yams.class.inc.php.
          YAMS: Yet Another Multilingual Solution for MODx
          YAMS Forums | Latest: YAMS 1.1.9 | YAMS Documentation
          Please consider donating if you appreciate the time and effort spent developing and supporting YAMS.
          • 22851
          • 805 Posts
          mgbowman. I have just got around to having a look at your recursive getDocumentIdentifier function. 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.
            YAMS: Yet Another Multilingual Solution for MODx
            YAMS Forums | Latest: YAMS 1.1.9 | YAMS Documentation
            Please consider donating if you appreciate the time and effort spent developing and supporting YAMS.
            • 19209
            • 18 Posts
            Ok let’s call this v0.0.3 - supports nested FURL paths with monolingual and multilingual resources as parents

            assets/modules/yams/yams-ux.class.inc.php
            <?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;
            		}
            
            	}
            
            }
            
            ?>


            Quote from: PMS at Aug 07, 2009, 06:14 AM

            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.)

            That uses the actual document’s id. At this stage we only have it’s resource alias or template variable alias. It’s either a 50/50 shot so I figure query by the template variable and if that fails fall back to querying by the resource’s alias. If that fails then it’s a malformed URL and you just let MODx take over.

            Quote from: PMS at Aug 07, 2009, 06:14 AM

            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.

            I don’t think we do. If you look at DocumentParser::sendForward() it calls DocumentParser::prepareResponse() as a result of the ’forward’. There it is already checking published status and deleted status and forwarding to an error page accordingly.

            Let me know what you think.
            --mgbowman
              • 22851
              • 805 Posts
              Brilliant. It’s looking better and better. Can I suggest a slight modification on your approach?

              Modify your queryDocumentIdentifierByMultilingualAlias and queryDocumentIdentifierByResourceAlias methods to return the document identifier AND the id of the parent document.

              Then, start parsing the path from the end rather than the start and go backwards. That way you can check whether a document is multi- or mono-lingual beforehand because you will have the document id.

              It is quite possible that someone created a multilingual document - associated it with multilingual template variables - and then turned it back into a monolingual document, leaving the mutlilingual tvs floating around (in case they wanted to switch back again, for example) - even though they are not used. Therefore your current approach of testing for the existence of multilingual tvs to determine whether a document is multilingual or not is not entirely safe.

              So, the algorithm would be something like:

              1. The final alias must belong to a multilingual document. Grab it’s docId and parentDocId using queryDocumentIdentifierByMultilingualAlias. Perhaps do a post-hoc check to see whether it really was a multilingual document using IsMultiLingualDocument, and return if not.

              Now the recursion starts...
              2. Check whether the parentDocId is multilingual or monolingual using IsMultiLingualDocument.
              3. Based on the answer, call queryDocumentIdentifierByMultilingualAlias or queryDocumentIdentifierByResourceAlias
              4. Keep looping until you have exhausted your array (success - forward) or encountered an inconsistency (failure - return).

                YAMS: Yet Another Multilingual Solution for MODx
                YAMS Forums | Latest: YAMS 1.1.9 | YAMS Documentation
                Please consider donating if you appreciate the time and effort spent developing and supporting YAMS.
                • 22851
                • 805 Posts
                YAMS Version 1.0.4 alpha RC 4 is now available for download. I recommend everyone to upgrade to this version. It includes fixes for several serious bugs, including one whereby you could get blank pages for largish documents and another whereby all pages are served with 404 not found!!! There is also more control over HTTP status codes now. This is very important for migrating an existing monolingual site to a multilingual site.

                I was going to improve/correct and add to the How To? documentation, but I didn’t get around to this with all the bug fixing.
                  YAMS: Yet Another Multilingual Solution for MODx
                  YAMS Forums | Latest: YAMS 1.1.9 | YAMS Documentation
                  Please consider donating if you appreciate the time and effort spent developing and supporting YAMS.
                  • 19209
                  • 18 Posts
                  Here’s v0.0.4

                  assets/modules/yams/yams-ux.plugin.inc.php
                  <?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();
                  			} 
                  		}
                  }
                  
                  ?>


                  assets/modules/yams/class/yams-ux.class.inc.php
                  <?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;
                  		}
                  	}
                  }
                  
                  ?>


                  So as you can see I abstracted the FURL routing into the plugin as it made more since there. The YAMS_UX class has 2 public API calls - getDocumentIdentifier($q) and getDocumentAlias($docId) - which I think is fitting of exactly what it’s doing.

                  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?.

                  What do you think?
                  --mgbowman
                    • 22851
                    • 805 Posts
                    Okay. Here are my thoughts/observations (my time to be nit-picky!)

                    1. With YAMS I have assumed that content/strings will be in either be an 8-bit or UTF-8 encoding. *** NOTE TO SELF: This restriction needs adding to the documentation *** (Perhaps I should have allowed for other multibyte encodings from the outset - and am willing to consider changing in future. If true multibyte support were needed then I would need to switch to the mbstring library and mb_ereg.) As a result I have avoided any standard php string functions - which are generally not multibyte safe. Instead I have used the preg library with or without the /u modifier (dependent on whether modx_charset is UTF-8 or not) to do everything that’s needed.

                    To be on the safe side, you might want to use the same approach (for the time being at least). You can get the encoding modifier (’’ or ’u’) from YAMS. Then

                    $modifier = $yams->GetEncodingModifier();
                    explode('/', $q)
                    // becomes
                    preg_split( '/\//' . $modifier, $q );
                    // and
                    str_replace('{parent}', $sql, $tmpl)
                    // becomes
                    preg_replace('/\{parent\}/' . $modifier, $sql, $tmpl)
                    


                    To be honest, I don’t think this will ever be a problem for your extension, since URLs are always encoded in a way that is insensitive to changes in encoding - but it doesn’t hurt to be over-cautious.

                    2. To be on the safe side we must use $modx->db->escape( $string ) on anything that gets inserted into an sql statement. This is most important for {alias} - since the user is effectively deciding this - and we wouldn’t want users to be able to inject code into our database by careful choice of URL.

                    3. I like your getDocumentIdentifier and getDocumentAlias approach. I was thinking this morning that I am going to need a getDocumentAlias function for building multilingual URLs and was going to ask you to structure your extension in such a way that I didn’t have to redo the work. It does mean that I will have to instantiate your class YAMS_UX from within YAMS and I can see that you are instantiating YAMS from with YAMS_UX. I don’t think this will cause a problem unless we both try to instantiate the other class in the constructor - so I wont do that - and we should be okay.

                    ... Hmm. It might make more sense if getDocumentAlias (or GetDocumentAlias to be consistent with the style I have adopted) lived in the YAMS core and YAMS_UX called it as a service. I’d include the relevant author and copyright attributions for that part of the core code of course in the header comments. Do you agree?

                    4. The ’virtual path’ will not have the language subdirectory name at the front of it - nor the subdirectory name if modx was installed into a subdirectory. They are removed, if they exist, by the .htaccess re-write rules. So you can remove the following line that tags the language subdirectory to the front of the alias generated:
                    $path[] = $this->lang;
                    

                    It will also mean that your extension will work in both root name and server name mode.

                    5.
                    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.

                    A general comment about this method is that you have already done all the hard work with recursion in putting together getDocumentAlias( $docId ). So you could just pop the end off your $aliases array, and get the docId for the final part of the alias, then build the correct URL for that alias using $requiredAlias = getDocumentAlias( $docId ), and do a simple string comparison:
                    $requiredAlias = $this->getDocumentAlias( $docId )
                    if ( $requiredAlias != $q )
                    {
                      return NULL;
                    }
                    else
                    {
                      return $docId;
                    }
                    

                    It might be very slightly less efficient - but it would mean that there is less code to maintain, which is a good thing. Also, I don’t know if there is a limit on the number of levels of embedded sql queries you are allowed, but this approach wouldn’t encounter that problem.

                    6. Are we going to allow for friendly alias paths to be on or off? If so, this logic could be built into getDocumentAlias using
                    if ( $this->modx->config['friendly_alias_urls'] == 1)
                    {
                    // the existing code
                    }
                    else
                    {
                    // only get the 'basename'
                    }
                    


                    So, after those comments are dealt with, all that’s required is to:
                    - test the plugin on its own to see if it forwards to correct document
                    - wire it up with YAMS (option in Module to enable multilingual aliases, yams placeholders outputting correct multilingual URLs)
                    - test the wired up version.

                    How desperately do you need this? Would it be okay to start the YAMS wiring next weekend, or would you prefer it earlier?
                      YAMS: Yet Another Multilingual Solution for MODx
                      YAMS Forums | Latest: YAMS 1.1.9 | YAMS Documentation
                      Please consider donating if you appreciate the time and effort spent developing and supporting YAMS.
                      • 19209
                      • 18 Posts
                      Ok here’s v0.0.5

                      assets/modules/yams/yams-ux.plugin.inc.php
                      <?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();
                            }
                          }
                      }
                      
                      ?>


                      assets/modules/yams/yams-ux.class.inc.php
                      <?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);
                          }
                      
                        }
                      
                      }
                      
                      ?>


                      Few things off the bat:

                      • I updated the code to more closely match that of YAMS core (to make migration easier) - the variable names are the same and I used wrapper functions so in my code there’s $this->IsMultilingualDocument(...) instead of $this->yams->IsMultilingualDocument(..)
                      • I think both GetDocumentAlias(..) and GetDocumentIdentifier(..) should be in YAMS core (YAMS_UX should be merged with YAMS)
                      • Regarding going from alias -> id I modified the algorithm according to your suggestions - grab the ’target’ alias and build the alias backwards, if it matches $q then we found what we’re looking for. The initial query looks at both resource alias and TV alias and loops over every match (just in case there’s multiple) - should I use something other than strcmp(..)?
                      • I abstracted all the queries into their own factory methods to increase readability & maintainability

                      Regarding if FURL alias paths are enabled, what do we do if they are disabled and there’s a ’/’ in $q? Do we simply look at the very first component and if we find a ’unique’ match (there could be multiples - 1 mono where alias=’foo’ & 1 multi where alias_en=’foo’) forward to the base resource? We could just abort altogether.

                      One more thought I had, what if FURLs are disabled altogether? Would we just return NULL for both GetDocumentAlias(..) and GetDocumentIdentifier(..)?

                      Looking forward to your reply,
                      --mgbowman