We launched new forums in March 2019—join us there. In a hurry for help with your website? Get Help Now!
    • 39932
    • 483 Posts
    This plugin allows you to add shared "children" pages to any Resource that has a Template. (Including Articles and ArticlesContainers). A common use for this would be to add an Edit page (with alias "edit") to all Articles using a single Resource. Another example would be to add a "buy" AJAX page to all pages of template Product. Proper use of this plugin can streamline development, reduce load times, and allow for better site support and debugging.

    See Also:



    Updated as of 8/19/2012

    • Simplified code logic for readability.
    • Supports my AJAX Framework now.
    • Added System Settings for Compatibility, Configuration, Security and Performance



    Summary

    Ever wonder how Articles does that neat stuff with URLs that you never knew you had? Well, it all comes down to aliasing. Articles creates false URLs and uses them whenever your container is in the requested URL. That's why if you add a path segment that Articles cannot process, it goes to the Container again and you may get different results. Extra URLs that provide additional content or functionality and you don't have to program? This is darn handy functionality! So, how do we replicate it without getting overly complicated?

    This tutorial will teach you how to make "Child Resources" that are added to every Resource of a given Template, just like Articles does for its containers and children (but in a much cleaner way). Child Resources may be full documents or AJAX partial HTML. They may even be Redirects or additional Scripts.



    The Process

    This tutorial looks longer than it takes to apply. You can complete the steps here in less than 10 minutes. First, we will create a Template. Next, we will create some System Settings (these will improve compatibility and performance). After the Settings are in place, it comes to the Plugin and its code. Then, testing begins for demonstration. This will involve creating a couple of Resources as Children. Finally, we'll address compatibility with other Plugins.



    Create the Action Template

    This part is simple and doesn't require any code, whatsoever. It uses yet another Template-aware trick to get the desired effect. Create a new Template. Name it "Alias (Template)". For ease, set the content to the below (this is optimal for AJAX calls, as well) and Save:

        [[*content]]
    




    System Settings

    We have to adjust one already provided System Setting, and then add a couple more. The System Settings we will be using will make the Plugin considerate to other Plugins (particularly our own). Additionally, one in particular, will reduce processing considerably if using Plugins from my other tutorials.

    (Required) Link to Template

    Go to System Settings in the MODx Manager. Click Create New Setting. Set the options as follows:

    • Key: id_template_action
    • Type: Template
    • Name: Alias: Template Action Template
    • Description: The ID of the Template that is used to define a sub-page for all parents of the same type as its parent.
    • Value: Alias (Template)

    Click Save and it is done. Note: It has come to my attention that Template Type is not always retrieved correctly between different MODx versions. In this case, set the type to Number. If it still doesn't work, there is another option above the code for the Plugin.

    (Required) Improve Processing

    If you are using Cross Context Resources (link in my signature), this is already set.

    Go to System Settings in the MODx Manager. In the search box, type 'key_doc_found' and press Enter. If it exists (from another tutorial), we're done. If it doesn't exist, click Create New Setting. Set the options as follows:

    • Key: key_doc_found
    • Type: Text
    • Name: Page Found Placeholder
    • Description: The key of the "found page" flag in the $_REQUEST global variable. This defaults to 'hasResource'.
    • Value: hasResource

    Click Save and it is done.

    (Recommended) Re-use Simple Aliases

    This needs to be set if you want to have simple aliases. An example is if you want 3 Templates to have an "edit" action, then this needs to be "Yes"

    Go to System Settings in the MODx Manager. In the search box, type "Use Friendly Alias Path".
    Double Click on the value and choose "Yes".

    (Optional) Allow Cross-Context Forwarding

    This needs to be set if you are going to have a single definition serve multiple contexts. If you are using Cross Context Resources (link in my signature), this is already set.

    Go to System Settings in the MODx Manager. In the search box, type 'allow_forward_across_contexts' and press Enter. Double-Click on the value and choose "Yes".



    Create the Plugin

    Create a new Plugin Element. Name it "onTemplateAction". Like my other Aliasing tricks, it uses OnPageNotFound as that is the event that will fire when you access the Action from a Resource that it is not specifically defined in. Now copy and paste the following code into the Content and Save:

    Important: Some versions of MODx do not read the Template type correctly. You have two options. 1) Convert id_template_action to a Number type; 2) Manually add the line "$idAction = #;" where # is the ID of Alias (Template). If you choose option 2, place the new line above the "$idAction = empty(...) ...;" (line 5)
    <?php
    //Get the System Settings (if we haven't already...)
        $keyURL = !empty($keyURL) ? $keyURL : $modx->getOption('key_url_request', null, 'toURL');
        $keyFound = !empty($keyFound) ? $keyFound : $modx->getOption('key_doc_found', null, 'hasResource');
        $idAction = !empty($idAction) ? $idAction : $modx->getOption('id_template_action', null, 'hasResource');
    
    // Get "passed" variables
        $isFound = empty($_REQUEST[$keyFound])
            ? 'false'
            : $_REQUEST[$keyFound];
    // Only do this if we need to scan.
        if ($isFound == 'false')
        {//See if a previous plugin has set the URL.
            $toURL = !empty($_REQUEST[$keyURL]) 
                ? $_REQUEST[$keyURL]
                : $_SERVER['REQUEST_URI'];
            $toParams = end(explode('?', $toURL));
            $segments = explode('/', trim($toURL, '/'));
        
            if (!empty($segments))
            {//Get the relevant alias
                $toAlias = trim(end($segments));
                if (empty($toAlias) || $toAlias == '')
                    $toAlias = strtolower(trim(prev($segments)));
                if (empty($toAlias) || $toAlias == '')
                    return;
            
            // Get the attached Resource
                $q = $modx->newQuery
                (   'modResource',
                    array
                    (   'alias'=>strval($toAlias),
                        'template'=>$idAction,
                        'published'=>1
                    )
                );
                $pages = $modx->getCollection('modResource', $q);
                if (!empty($pages))
                {//Get the Parent Resource
                    $toParent = trim(prev($segments));
                    if (!empty($toParent))
                    {    $q = $modx->newQuery
                        (   'modResource',
                            array
                            (   'alias'=>strval($toParent),
                                'published'=>1,
                                'context_key'=>$modx->context->key
                            )
                        );
                        $toParent = $modx->getObject('modResource', $q);
                        if (!empty($toParent))
                        {   $forID = $toParent->get('id');
                            $toParent = $toParent->get('template');
                        }
                    }
                    //    if (empty($toParent) || $toParent == '' || $toParent == 0)
                    //        return;
                        
                // Compare the results
                    foreach ($pages as $key => $res)
                    {   if (!empty($res))
                        {   $chkParent = $res->getOne('Parent')->get('template');
                            if ($chkParent == $toParent)
                            {   $_REQUEST[$keyFound] = 'true';
                                $_REQUEST['forID'] = $forID;
                                $modx->sendForward($res->get('id'));
                                break;
                            }
                        }
                    }
                }
            }
        }
    


    Click the "System Events" tab and scroll down to OnPageNotFound. Check the box, and choose a priority of 3 (or higher). Click Save. Note: If you have: a) my AJAX Framework plugins, my other Custom Alias plugins, or the Articles Add-on, see the section On Compatibility.



    Testing the Plugin

    This testing process is a little involved and requires you to make a choice or two. This is because the versatility and flexibility of MODx allows just a single Template for many purposes or many Templates for singular purposes.

    Test Setup: Action Containers[/u]

    Look through your Template list. If you have multiple Templates, good job! Pick two that have at least one Resource that is accessible in your Resource Tree. Note: Do not pick Article, ArticleContainer or Alias (Site). While it will work with all of them, this will only complicate the tests and confuse you, at first. For Articles and ArticlesContainers, you must create a new one without using the CMP and by setting the template manually.

    Test Setup: Create the Actions

    For each of your Templates chosen above, pick a Resource that uses that Template (you will do the following for each one). Inside it, make a Resource. Set a unique title for each. For demonstration, set the alias for each to "test-action". This should not conflict as they are under different parents. Set the Templates to "Alias (Template)". After it reloads, set the content for both to:

        <h3>[[*pagetitle]]</h3>
    


    Test One: Test the Actions

    Right Click on each of the Actions and choose "View Resource". Each should show up with its respective title.

    Test Two: Test another Resource

    Pick another Resource of one of your Template from above. Right-click on it and choose "View Resource". After it loads, go to the Address Bar of your browser. Add "/test-action/" to the end and press Enter. You should see the appropriate *pagetitle of the correct Action.

    Rinse and repeat for the second template from the setup. Once you view the resource, and go to "test-action", you should see the other *pagetitle! Now you have reused functionality but made it specific to each template.



    On Compatibility

    Supports

    Does not interfere with GET or POST.

    Supports Static Resources as Parents (haven't tested this with Children). Since our call uses sendForward, static resources are no problem.

    Works across contexts, if you want to keep your "object/class" definitions separate from the documents. You may split aliases between multiple resources of the same template, though it is not recommended.

    All Content Types and their content are supported. If you use other Content Types, depending on setup you may run into issues with extensions or extra/missing slashes. If this is the case, you may also install my Remove Extension and Slash Issues in URLs Plugin. (link in signature)

    System Settings

    The System Settings used here (getOption()) are used by my other Plugins, as well. You don't need their Settings unless you are using their Plugins. If you would like to create them, refer to the updated Tutorial on my AJAX Framework (link in my signature).

    FuzzicalLogic's AJAX Tutorials

    Priority for this plugin must be after (higher than) all other AJAX Framework plugins that use OnPageNotFound. This includes the following: onGetRequestType, onParseURLParams. For ensured compatibility, make sure you "upgrade" using the updated Tutorial. (links in my signature)

    FuzzicalLogic's Aliasing Tutorials

    Priority for this plugin must be after (higher than) Cross Context Resources. While it is not required to upgrade this plugins, updating provides much better compatibility, tighter security, optimized processing, and configuration options. (links in my signature)

    Articles Plugin

    If you use Articles, you will need to change the priority of ArticlesPlugin to be after all of my plugins. (also see Further Exploration below)



    Further Exploration

    If you are using AJAX Framework, you now have shared actions without having to include an additional parameter (URL or otherwise). Additionally, the snippets they call may exhibit entirely different functionality.

    If you use Articles and have multiple Container Templates, you can have different actions for each. To avoid "losing" the Aliases in the Resource Tree, you may create a blank Resource. Set the Template to your Container and add the actions to it.

    You may add actions to custom user pages!

    Front-end editing can be more secure by being tied only to the documents that they should be. The Parent ID is automatically sent to the Action, so there is no need to send it in a GET, POST or URL parameter. Simply remove access to the link and perform a little security checking.

    Further, this single URL may be excluded from your site-map easily.



    Conclusion

    This technique has made my life so much easier. No more duplication of code/functionality. Additionally, I have a separate Context (with no url) with nothing but blank resources containing Template Actions for "class definition". Super clean and tight functionality. [ed. note: fuzzicallogic last edited this post 14 years, 1 month ago.]
      Website: Extended Dialog Development Blog: on Extended Dialog
      Add-ons: AJAX Revolution, RO.IDEs Editor & Framework (in works) Utilities: Plugin Compatibility List
      Tutorials: Create Cross-Context Resources, Cross-Context AJAX Login, Template-Based Actions, Remove Extensions from URLs

      Failure is just another word for saying you didn't want to try. "It can't be done" means "I don't know how".
      • 39932
      • 483 Posts
      Explanation of Code

      Quick Link:Top of Tutorial

      The code used in this Plugin is pretty simple and not as complex or tricksy as some of the other plugins. Here, we see several useful examples. Those who come from Object Oriented backgrounds will see how this makes Templates a little more OO (for complex sites). The code shows the following:

      • Querying via xPDO
      • Retrieving System Settings
      • Getting/Setting passed data to other MODx objects
      • Content Forwarding via sendForward()

      Like the other plugins, we are getting System Settings for the first few lines. Since we need this to be compatible, there are a few important things happening here. First, Template Actions may not be the first plugin that runs. This means the URL may or may not be set by another compatible plugin. As stated in other tutorials, we pass information from one plugin to another via the $_REQUEST array. This only works for a single URL request (so sendRedirect() generally does not suffice).

      <?php
      //Get the System Settings (if we haven't already...)
          $keyURL = !empty($keyURL) ? $keyURL : $modx->getOption('key_url_request', null, 'toURL');
          $keyFound = !empty($keyFound) ? $keyFound : $modx->getOption('key_doc_found', null, 'hasResource');
          $idAction = !empty($idAction) ? $idAction : $modx->getOption('id_template_action', null, 'hasResource');
      


      Now, just in case another plugin has found a page, we conform and do not run if the $_REQUEST key is set. Simply put, we don't run if we don't have to.

      // Get "passed" variables
          $isFound = empty($_REQUEST[$keyFound])
              ? 'false'
              : $_REQUEST[$keyFound];
      // Only do this if we need to scan.
          if ($isFound == 'false')
          {
      


      Now, we get the modified URL if it was set. The avoid tampering with $_GET and $_POST, we remove everything after the "?" for our purposes. Notice we don't set the URL, just retrieve it. Then we take the result, and split the path segments from each other.

      //See if a previous plugin has set the URL.
              $toURL = !empty($_REQUEST[$keyURL]) 
                  ? $_REQUEST[$keyURL]
                  : $_SERVER['REQUEST_URI'];
              $toParams = end(explode('?', $toURL));
      
              $segments = explode('/', trim($toURL, '/'));
      


      If we have no segments, then obviously we are at the home of the site. No alias finding is necessary. Otherwise, we trim the segments (in case of typos and user typing). The alias will always be the last segment, except in special conditions. Now, let's talk about compatibility, because this is where we get to see the power of passing data between successive Plugins.

      Template Actions runs after AJAX Framework. Template Actions do not care about AJAX aliases, so the AJAX Plugin takes them out of the URL Request. Since AJAX doesn't actually find the document, it converts the AJAX part of the URL to params, leaving the rest to the Plugins that actually find the document. This means Template Actions will always be at the end of the modified URL. Now, we could have done what Cross Context Plugin does and allowed access to children, but this didn't seem smart. After all, a Site Alias page may have Template Actions (not recommended), but a Template Action may not be a Site Alias. It already "crosses-contexts", only with regard to a specific parent.

              if (!empty($segments))
              {//Get the relevant alias
                  $toAlias = trim(end($segments));
                  if (empty($toAlias) || $toAlias == '')
                      $toAlias = strtolower(trim(prev($segments)));
                  if (empty($toAlias) || $toAlias == '')
                      return;
      


      Now, an xPDO Query to get all Alias (Templates) that match our Alias. Why do we get all of them? Here, we make another choice. Any Template could have a Template Action that potentially matches another. We need to find the right Action to forward to (an Action is a page, remember). We could do all of the work of finding information about the Parent's Template first. However, the browser request needs to be as fast as possible. So, instead, we get the list. We do the comparison soon.

              // Get the attached Resource
                  $q = $modx->newQuery
                  (   'modResource',
                      array
                      (   'alias'=>strval($toAlias),
                          'template'=>$idAction,
                          'published'=>1
                      )
                  );
                  $pages = $modx->getCollection('modResource', $q);
      


      Here, we get the Resource of the parent segment using the alias. This will be used for the actual comparison. Since we don't actually know if the Alias in a real child, we cannot use the xPDO getOne. Enter xPDO Queries, yet again. We use the segment previous to the alias above for this query.

                  if (!empty($pages))
                  {//Get the Parent Resource
                      $toParent = trim(prev($segments));
                      if (!empty($toParent))
                      {    $q = $modx->newQuery
                          (   'modResource',
                              array
                              (   'alias'=>strval($toParent),
                                  'published'=>1,
                                  'context_key'=>$modx->context->key
                              )
                          );
                          $toParent = $modx->getObject('modResource', $q);
                          if (!empty($toParent))
                          {   $forID = $toParent->get('id');
                              $toParent = $toParent->get('template');
                          }
                      }
                      //    if (empty($toParent) || $toParent == '' || $toParent == 0)
                      //        return;
                           
      


      We're here!! The actual comparison. By the time we get here, we have the pathed parent Resource and all of the Aliases. So for each Template Action, we get its real Parent, and then check if its Template matches the pathed parent. If it does, we have succeeded. Now, we just have to make sure that we send the pathed parent's ID for some awesome processing. (Since AJAX already makes the parameters, we don't worry about those.)

                  // Compare the results
                      foreach ($pages as $key => $res)
                      {   if (!empty($res))
                          {   $chkParent = $res->getOne('Parent')->get('template');
                              if ($chkParent == $toParent)
                              {   $_REQUEST[$keyFound] = 'true';
                                  $_REQUEST['forID'] = $forID;
                                  $modx->sendForward($res->get('id'));
                                  break;
                              }
                          }
                      }
                  }
              }
          }
      


      When it is all said and done, if we found a match, we let the other Plugins know in the same way we would have been told. We then forward directly to the document. If we did not find one, we get the special 404 Error. Simple as that. [ed. note: fuzzicallogic last edited this post 14 years, 1 month ago.]
        Website: Extended Dialog Development Blog: on Extended Dialog
        Add-ons: AJAX Revolution, RO.IDEs Editor & Framework (in works) Utilities: Plugin Compatibility List
        Tutorials: Create Cross-Context Resources, Cross-Context AJAX Login, Template-Based Actions, Remove Extensions from URLs

        Failure is just another word for saying you didn't want to try. "It can't be done" means "I don't know how".
        • 28042 ☆ A M B ☆
        • 24,524 Posts
        I'm feeling like a rank newbie again; I know the words but don't understand the context (no pun intended). After reading it three times I still have no idea what it's supposed to do. I suppose I'll just have to work through the tutorial, then maybe I'll get the point.
          Studying MODX in the desert - http://sottwell.com
          Tips and Tricks from the MODX Forums and Slack Channels - http://modxcookbook.com
          Join the Slack Community - http://modx.org
          • 28042 ☆ A M B ☆
          • 24,524 Posts
          Um... shouldn't that be

          <h3>[[*pagetitle]]</h3>[


          (I'm confused enough that this typo actually took me a few minutes to figure out!)

          I also had to turn on Use Friendly Alias Path in order for the system to allow me to have duplicate aliases at all. [ed. note: sottwell last edited this post 14 years, 1 month ago.]
            Studying MODX in the desert - http://sottwell.com
            Tips and Tricks from the MODX Forums and Slack Channels - http://modxcookbook.com
            Join the Slack Community - http://modx.org
            • 28042 ☆ A M B ☆
            • 24,524 Posts
            Sorry, I get my Page Not Found page.

            I have "free.sottwell.modxcloud.com/one.html", and to this I'm supposed to add "/test-action/" so I end up with "http://free.sottwell.modxcloud.com/one.html/test-action/" ?

            Is this only useful in the case of multiple templates and multiple contexts?
              Studying MODX in the desert - http://sottwell.com
              Tips and Tricks from the MODX Forums and Slack Channels - http://modxcookbook.com
              Join the Slack Community - http://modx.org
              • 39932
              • 483 Posts
              @Susan:

              Um... shouldn't that be...

              Nope. It is and should only be:

              <h3>[[*pagetitle]]</h3>
              


              No extra [

              ... so I end up with "http://free.sottwell.modxcloud.com/one.html/test-action/"

              Yes. but depending on your Settings, you may get Page Not Found. That is why I made the Remove Extensions and Slash Issues from URLs. All it does is gets rid of the extensions for the purposes of resource finding. It still uses them where ever they are actually present. Here is a link: http://goo.gl/VZ82E

              If you set test-action as not a Container, you may have to add "/test-action.html" instead. Crap I have to adjust the Tutorial. Thanks for pointing this out.

              As an option, you may change both to Containers. This will fix it.

              Is this only useful in the case of multiple templates and multiple contexts?

              Multiple Contexts: No. Multiple templates: In general, yes. The idea is to share functionality. However, it will work with just one template.

              I know the words but don't understand the context

              This is not important for you, but can be helpful. A context is like another website.... A sub-domain can be hosted as a Context. A separate domain could be hosted as a Context. A context could also be a copy of the site with a different language... Even further, a Context could simply be a part of the database that cannot be accessed by a URL at all.

              I also had to turn on Use Friendly Alias Path...

              Whoa!! Totally forgot about this Setting. I set it on install and haven't looked at it since. I'll fix that. [ed. note: fuzzicallogic last edited this post 14 years, 1 month ago.]
                Website: Extended Dialog Development Blog: on Extended Dialog
                Add-ons: AJAX Revolution, RO.IDEs Editor & Framework (in works) Utilities: Plugin Compatibility List
                Tutorials: Create Cross-Context Resources, Cross-Context AJAX Login, Template-Based Actions, Remove Extensions from URLs

                Failure is just another word for saying you didn't want to try. "It can't be done" means "I don't know how".
                • 28042 ☆ A M B ☆
                • 24,524 Posts
                Heh. I'm such a troublemaker! I'm good at finding the nit-picky trouble spots. Just ask opengeek wink

                Anyway, still just getting the not-found page; changed everything to a container and used http://free.sottwell.modxcloud.com/one/testing-three/test-action/.

                I still have no idea what this is supposed to do.

                Nope. It is and should only be:
                Check your tutorial... you have (I know it's not just my wretched eyes, I copy/pasted this directly from the first post above).
                <h3>[[*pagetitle]]<\h3>


                This is not important for you, but can be helpful. A context is like another website.... A sub-domain can be hosted as a Context.
                I was referring to the context of the words used in the description of this feature, not a MODx Context.

                  Studying MODX in the desert - http://sottwell.com
                  Tips and Tricks from the MODX Forums and Slack Channels - http://modxcookbook.com
                  Join the Slack Community - http://modx.org
                  • 39932
                  • 483 Posts
                  @Susan:

                  Since we got a quick response in there, now I can go more in depth with you about Contexts.

                  Like I said before: a Context is like another website. This "other" website can be used in many, many ways. It shares Users, Groups, and Security policy with all other Contexts in the installation. However, access policies can be set for each Context and Resource Group.

                  All Elements are shared between Contexts. So "functionality" is in tact no matter which "site" you are on.

                  System Settings serve as the default settings for all of your Contexts. Contexts may override System Settings, however (similar to Property Sets vs. Default Properties).

                  Resources only exist in the Context they were created in. Here in lies the crux for many people. If you want an AJAX Resource that is accessible from every sub-domain, you must use a Custom Alias solution, or b) copy the resource every time, c) use jsonp as it is a cross-domain request, or d) use an iframe. In other words, either a LOT of work or no straight HTML response.

                  This plugin is not important for Contexts, but does bridge them if you have them. However, it only does so for Alias (Templates) with the same parent.

                  Does this help?
                    Website: Extended Dialog Development Blog: on Extended Dialog
                    Add-ons: AJAX Revolution, RO.IDEs Editor & Framework (in works) Utilities: Plugin Compatibility List
                    Tutorials: Create Cross-Context Resources, Cross-Context AJAX Login, Template-Based Actions, Remove Extensions from URLs

                    Failure is just another word for saying you didn't want to try. "It can't be done" means "I don't know how".
                    • 39932
                    • 483 Posts
                    @Susan:

                    I copy/pasted this directly from the first post above).

                    Ah ha! Your original post showed an extra [ and I got hung up on that. I totally missed the wrong direction slash!!
                      Website: Extended Dialog Development Blog: on Extended Dialog
                      Add-ons: AJAX Revolution, RO.IDEs Editor & Framework (in works) Utilities: Plugin Compatibility List
                      Tutorials: Create Cross-Context Resources, Cross-Context AJAX Login, Template-Based Actions, Remove Extensions from URLs

                      Failure is just another word for saying you didn't want to try. "It can't be done" means "I don't know how".
                      • 39932
                      • 483 Posts
                      @Susan:

                      This link works: http://free.sottwell.modxcloud.com/one/test-action/

                      Now, do you have another Resource of the same Template as "one"? If so, add "test-action" to that URL.
                        Website: Extended Dialog Development Blog: on Extended Dialog
                        Add-ons: AJAX Revolution, RO.IDEs Editor & Framework (in works) Utilities: Plugin Compatibility List
                        Tutorials: Create Cross-Context Resources, Cross-Context AJAX Login, Template-Based Actions, Remove Extensions from URLs

                        Failure is just another word for saying you didn't want to try. "It can't be done" means "I don't know how".