We’re in the middle of a series covering the various Magento sub-systems responsible for routing a URL to a particular code entry point. So far we’ve covered the general front controller architecture and the Standard
Magento router object. Today we’re covering the three remaining stock router objects. You may want to bone up on parts one and two before continuing.
Notice: While the specifics of this article refer to the 1.6
branch of Magento Community Edition, the general concepts apply to all versions.
The Admin Router Object
Today we’ll start with the admin router object, defined by the Mage_Core_Controller_Varien_Router_Admin
class
#File: app/code/core/Mage/Core/Controller/Varien/Router/Admin.php
class Mage_Core_Controller_Varien_Router_Admin extends Mage_Core_Controller_Varien_Router_Standard
{
...
}
Of the four routers that ship with Magento, the admin router object is the first router object instantiated and checked for a match
. This is the router object responsible for dispatching requests to the admin console application, which is sometimes known as the adminhtml
, (not admin
) Magento area
. Naming gets a little tricky this deep into the system, as many of the concepts in the initial Magento system evolved into the system we have today.
Let’s get started.
Matchmaker Matchmaker, Make m– hello?
If you open the Admin.php
file mentioned above, the first thing you’ll notice is there’s no match
method defined. After a little head scratching, you’ll realize the obvious: The match
method must be defined on the Admin
router’s parent class. We’ll just hop up a level to find the match
method
#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
class Mage_Core_Controller_Varien_Router_Standard extends Mage_Core_Controller_Varien_Router_Abstract
{
...
public function match(Zend_Controller_Request_Http $request)
{
...
}
...
}
Take a closer look at the parent classname. It’s Mage_Core_Controller_Varien_Router_Standard
, which is also the class of our second router, and as we’ve learned previously contains the routing logic for the frontend application.
In some ways this is good. It means the admin console application and the cart application share the same routing logic. However, the way things are architected lends itself to some confusion, and leads to a lot of special case statements for the Admin
router inside the Standard
router’s match
method.
In retrospect, a more “Magento like” approach would have been a base Standard
router class, (probably Abstract), that contained shared routing logic, and then a Cart
router for the frontend cart application, and an Admin
router for the admin console application. We mention this mainly as a reminder that the metaphors on this level are a little muddled, and the logical conclusions your programmer trained brain will jump to might be the wrong logical conclusion.
Rather than run through the entire routing match
logic again, we’re going to highlight the differences in the Admin
routing, as well as the rational for some of the special cases.
It’s the Little Differences
First up is the _beforeModuleMatch
method. You’ll recall this method is one of the first things called in the match
method. In the Admin
router, this always returns true.
#File: app/code/core/Mage/Core/Controller/Varien/Router/Admin.php
protected function _beforeModuleMatch()
{
return true;
}
However, if we pop down to the Standard
router, we see
#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
protected function _beforeModuleMatch()
{
if (Mage::app()->getStore()->isAdmin()) {
return false;
}
return true;
}
So it looks like this method is here to ensure the Standard
router object bails if, for some reason, the store model object thinks its in admin mode. Like the Standard
/Admin
router relationship, the store
object is another one of those things that points to certain parts of the Magento development process being focused on the frontend application first, and then tacking on an admin console later and having to back-port changes.
The store object is a model that really only applies to the frontend/cart application. However, because so much code in Magento assumes the store object exists, it needs to be available to the admin console application. This, in turn, creates trouble at the router level, which is what leads to a check like this. Many layers of abstraction, no defined contracts between classes/modules, and the fear of refactoring created by the lack of tests will always lead to these sorts of situations.
The next difference to consider is the related _afterModuleMatch
method. As a reminder, here’s how it’s used in the match
method
#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
//checkings after we foundout that this router should be used for current module
if (!$this->_afterModuleMatch()) {
return false;
}
In the admin router class, this _afterModuleMatch
method is defined as
#File: app/code/core/Mage/Core/Controller/Varien/Router/Admin.php
/**
* checking if we installed or not and doing redirect
*
* @return bool
*/
protected function _afterModuleMatch()
{
if (!Mage::isInstalled()) {
Mage::app()->getFrontController()->getResponse()
->setRedirect(Mage::getUrl('install'))
->sendResponse();
exit;
}
return true;
}
This code will redirect a user to the installation wizard the first time Magento is run. While it doesn’t have anything to do with the admin console routing, it’s here because the admin routing object is the first router object that’s checked. In hindsight, this logic probably belongs somewhere higher up the stack, perhaps in the front controller object. It’s also arguable there should be a Installer
routing object that’s checked first. The third argument, and the one that always wins, is to leave the code where it is, because it works. You know the drill.
Moving on, the admin router object also defines its own fetchDefault
method.
#File: app/code/core/Mage/Core/Controller/Varien/Router/Admin.php
public function fetchDefault()
{
// set defaults
$d = explode('/', $this->_getDefaultPath());
$this->getFront()->setDefault(array(
'module' => !empty($d[0]) ? $d[0] : '',
'controller' => !empty($d[1]) ? $d[1] : 'index',
'action' => !empty($d[2]) ? $d[2] : 'index'
));
}
You’ll recall that this is the method that sets a default module name, controller name, and action name on the front controller, which the match
method will ask for if it can’t derive a name. As you can see, the Admin
router gets these defaults by calling _getDefaultPath
, which pulls the path directly from the merged Magento config.xml
tree
#File: app/code/core/Mage/Core/Controller/Varien/Router/Admin.php
protected function _getDefaultPath()
{
return (string)Mage::getConfig()->getNode('default/web/default/admin');
}
You’ll recall the _getDefaultPath
method is used by the standard router, but not to set these defaults. Instead it’s used to provide path information for the root level URL. Again, we’re seeing a situation that seems like different camps with different visions coming into conflict with one another leading to muddled metaphors.
Similarly, the _shouldBeSecure
method also needs some custom admin code to apply a slightly different set of logic based on values fetched from the admin config settings
#File: app/code/core/Mage/Core/Controller/Varien/Router/Admin.php
protected function _shouldBeSecure($path)
{
return substr((string)Mage::getConfig()->getNode('default/web/unsecure/base_url'),0,5)==='https'
|| Mage::getStoreConfigFlag('web/secure/use_in_adminhtml', Mage_Core_Model_App::ADMIN_STORE_ID)
&& substr((string)Mage::getConfig()->getNode('default/web/secure/base_url'),0,5)==='https';
}
and the same goes for _getCurrentSecureUrl
#File: app/code/core/Mage/Core/Controller/Varien/Router/Admin.php
protected function _getCurrentSecureUrl($request)
{
return Mage::app()->getStore(Mage_Core_Model_App::ADMIN_STORE_ID)
->getBaseUrl('link', true) . ltrim($request->getPathInfo(), '/');
}
Still more examples where the needs of the application continue to permeate into the base framework/system code.
Raiders of the Lost 404
Next up is the _noRouteShouldBeApplied
method (as defined in the Admin
router)
#File: app/code/core/Mage/Core/Controller/Varien/Router/Admin.php
protected function _noRouteShouldBeApplied()
{
return true;
}
In the Standard
router class, _noRouteShouldBeApplied
was hard coded to return false
. This method comes into play late in the match
logic. If we have a valid module, but are unable to find a controller and action match, we called _noRouteShouldBeApplied
as follows
#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
if (!$found) {
if ($this->_noRouteShouldBeApplied()) {
$controller = 'index';
$action = 'noroute';
$controllerClassName = $this->_validateControllerClassName($realModule, $controller);
if (!$controllerClassName) {
return false;
}
// instantiate controller class
$controllerInstance =
Mage::getControllerInstance($controllerClassName, $request, $front->getResponse());
if (!$controllerInstance->hasAction($action)) {
return false;
}
} else {
return false;
}
}
In the Standard
router, this branch is always skipped. However, in the Admin
router things get interesting. This appears to be an older code path that, if no controller or action was found for a module, would make one last ditch effect to look at the Magento module’s index controller (if it exists), and call a noRoute
action method. My guess is this was meant to provide the admin console with a 404 page for its own for urls, such as
http://magento.example.com/admin/notthere
However, this appears to have been broken at some point. If you look at the call to create and validate a controller class name
$this->_validateControllerClassName($realModule, $controller);
you can see its using the last $realModule
that was found in the main foreach
loop. At some point in the release history, Magento added the Phoenix_Moneybookers
module to its default distribution. This module configures a second $realModule
for the router to check. Because this module has no index controller, that means this particular branch will always return false, because $realModule
will always be Phoenix_Moneybookers
at this point in the code.
It’s easy to see how fixing a bug like this gets de-prioritized over building actual features, but it’s equally easy to see how this additional, now obsolete, code branch makes the entire routing process that much more opaque.
For what it’s worth, you can see the noroute
action in the base Admin
controller here
#File: app/code/core/Mage/Adminhtml/Controller/Action.php
public function norouteAction($coreRoute = null)
{
$this->getResponse()->setHeader('HTTP/1.1','404 Not Found');
$this->getResponse()->setHeader('Status','404 File not found');
$this->loadLayout(array('default', 'adminhtml_noroute'));
$this->renderLayout();
}
This, in turn, triggers the following layout update XML, setting a 404 message
<!-- File: adminhtml/default/default/layout/main.xml -->
<adminhtml_noroute>
<reference name="content">
<block type="core/text" name="content.noRoute">
<action method="setText" translate="text" module="adminhtml">
<text>
<![CDATA[
<h1 class="page-heading">404 Error</h1>
<p>Page not found.</p>
]]>
</text>
</action>
</block>
</reference>
</adminhtml_noroute>
Route Collecting
Finally, we have what many would consider the main difference in the Admin
router, collecting routes.
First off, when collectRoutes($configArea, $useRouterName)
is called during router instantiation, both the $configArea
and $useRouterName
are set to the string admin
. This means our collectRoutes
method will be looking for routers at
admin/routers
instead of
frontend/routers
and will be looking for a node with the value
<use>admin</use>
instead of
<use>standard</use>
In addition to this, collectRoutes
is redefined to do some jiggery pokery before calling back to the parent collectRoutes
#File: app/code/core/Mage/Core/Controller/Varien/Router/Admin.php
public function collectRoutes($configArea, $useRouterName)
{
if ((string)Mage::getConfig()->getNode(Mage_Adminhtml_Helper_Data::XML_PATH_USE_CUSTOM_ADMIN_URL)) {
$customUrl = (string)Mage::getConfig()->getNode(Mage_Adminhtml_Helper_Data::XML_PATH_CUSTOM_ADMIN_URL);
$xmlPath = Mage_Adminhtml_Helper_Data::XML_PATH_ADMINHTML_ROUTER_FRONTNAME;
if ((string)Mage::getConfig()->getNode($xmlPath) != $customUrl) {
Mage::getConfig()->setNode($xmlPath, $customUrl, true);
}
}
parent::collectRoutes($configArea, $useRouterName);
}
This additional if
block implements the custom admin url feature, configured at
System -> Configuration -> Admin -> Custom Admin Path
This is interesting for a few reasons. The first is, the developer who worked on this feature didn’t use the provided API for accessing store configuration variables, Mage::getStoreConfig
. Instead they dove directly into the configuration tree. The results are the same, but it either points to a difference of opinion about the best way to access config variables or that using getStoreConfig
, which triggers instantiation of a store, caused yet another conflict with something because we’re not on the frontend application.
The second reason this is interesting is how the feature was implemented. Rather than monkey with the request to achieve a new admin URL, the developer dives into the already merged configuration tree and changes the value at
admin/routers/adminhtml/args/frontName
This alters the adminhtml
router’s configuration node at runtime such that this
<!-- File: app/code/core/Mage/Core/Adminhtml/etc/config.xml -->
<adminhtml>
<use>admin</use>
<args>
<module>Mage_Adminhtml</module>
<frontName>admin</frontName>
</args>
</adminhtml>
will look like this
<!-- never in a file, just in memory or cache -->
<adminhtml>
<use>admin</use>
<args>
<module>Mage_Adminhtml</module>
<frontName>secret_admin</frontName>
</args>
</adminhtml>
That’s extremely unorthodox, but also very clever. I imagine whomever came up with it has some epic battles with the guys who said adding a custom admin URL would be too much work and introduced too much risk to the existing codebase.
Admin Wrap Up
Other than the above exceptions, the Admin
routing code behaves identical to the Standard
routing code. The configuration is searched for potential modules, then the potential modules are searched for a matching controller and action, and if found, the action is dispatched.
That said, although it’s possible to define multiple front names for the admin router, I’d advise against doing so (despite previous advice). There are parts of the admin console application that assume a front name of admin
. While pages will load and work with alternate admin frontnames, certain features (such as the media gallery integration for the rich text editor), may not.
While it’s possible to work around these special cases, you’re better off slipping your module into the admin
front name/adminhtml
config router node, and then being careful there’s no overlap between your controller names and the Mage_Adminhtml
module controller names.
The CMS Router Object
We’ve reached our third router object, the Cms
router, defined by the class Mage_Cms_Controller_Router
.
The Cms
router is a little different from the others. First off, it’s added to the system via a listener for the controller_front_init_routers
event. Additionally, it doesn’t inherit from the Standard
router class, and its purpose isn’t to determine which action controller to use. Instead, the Cms
router’s purpose is to determine if the request URL matches a content page in the database, and if so ensure a page is rendered with that content. Earlier we said that the Cms
router kicked off an MVC system. That was a bit of a fib to smooth over some of the inherent complexity of Magento’s routing system.
If that didn’t make sense don’t worry, all will become clear. Let’s get right to it.
Entering the Cms
router’s match
method, the first thing we see is a quick, paranoid sanity check to ensure first-run users are sent to the installer.
#File: app/code/core/Mage/Cms/Controller/Router.php
public function match(Zend_Controller_Request_Http $request)
{
if (!Mage::isInstalled()) {
Mage::app()->getFrontController()->getResponse()
->setRedirect(Mage::getUrl('install'))
->sendResponse();
exit;
}
...
}
It’s not certain if this is just healthy paranoia, or if there were certain cases where other redirects to the installer failed and one got through to the Cms
router. Sound familiar?
Irrespective of all that, we can safely ignore this block of code and move on to the next.
#File: app/code/core/Mage/Cms/Controller/Router.php
$identifier = trim($request->getPathInfo(), '/');
Although the syntax is slightly different, we’re still on track with the other router objects here. We’re grabbing the normalized path information of the URL. A URL like this
http://magento.example.com/about-magento-demo-store
will result in an $identifier
containing the string about-magento-demo-store
. Next up though
#File: app/code/core/Mage/Cms/Controller/Router.php
$condition = new Varien_Object(array(
'identifier' => $identifier,
'continue' => true
));
Mage::dispatchEvent('cms_controller_router_match_before', array(
'router' => $this,
'condition' => $condition
));
is something a little different. Before trying to match, Magento fires off an event. The $condition
object is a transport object, used to send information off to the event listeners.
#File: app/code/core/Mage/Cms/Controller/Router.php
$identifier = $condition->getIdentifier();
if ($condition->getRedirectUrl()) {
Mage::app()->getFrontController()->getResponse()
->setRedirect($condition->getRedirectUrl())
->sendResponse();
$request->setDispatched(true);
return true;
}
if (!$condition->getContinue()) {
return false;
}
After the event fires, we examine the object in $condition
. First, we pull out the identifier. Secondly, we check the object for a redirect_url
property (magic getters/setters), and if it exists we use it to fire off an HTTP redirect. Finally, if the condition
property has been set to false, the Cms
router stops trying to match, and passes control on to the next router object by returning false.
There’s a few things here that could use some clarification. First, by looking at this code, we can infer what the intention of the cms_controller_router_match_before
event is. It allows users to intercept a CMS page request and based on custom programatic rules in the observer
- Change the page that’s going to be rendered
- OR trigger an HTTP redirect
- OR halt the request
Secondly, the redirection code
#File: app/code/core/Mage/Cms/Controller/Router.php
Mage::app()->getFrontController()->getResponse()
->setRedirect($condition->getRedirectUrl())
->sendResponse();
$request->setDispatched(true);
return true;
can be a little confusing. If the redirect_url
property is an external http
url, Magento will redirect with a Location
header and while the browser goes off to another page, the rest of the system code executes in the background. The response object’s sendResponse
doesn’t automatically exit
for a web server based redirection.
Next up, if we’re still here, we use the identifier to load a CMS model from the system.
#File: app/code/core/Mage/Cms/Controller/Router.php
$page = Mage::getModel('cms/page');
$pageId = $page->checkIdentifier($identifier, Mage::app()->getStore()->getId());
It might be more appropriate to say we fetch the database id of the CMS page that matches the identifier, since we don’t actually load
the model. You can checkout the resource model method that ultimately gets called if you’re interested in the details
#File: app/code/core/Mage/Cms/Model/Resource/Page.php
public function checkIdentifier($identifier, $storeId)
{
$stores = array(Mage_Core_Model_App::ADMIN_STORE_ID, $storeId);
$select = $this->_getLoadByIdentifierSelect($identifier, $stores, 1);
$select->reset(Zend_Db_Select::COLUMNS)
->columns('cp.page_id')
->order('cps.store_id DESC')
->limit(1);
return $this->_getReadAdapter()->fetchOne($select);
}
As you can see, it’s a simple select to the CMS database table.
Dispatching a CMS Page
Up next is this bit of code
#File: app/code/core/Mage/Cms/Controller/Router.php
if (!$pageId) {
return false;
}
$request->setModuleName('cms')
->setControllerName('page')
->setActionName('view')
->setParam('page_id', $pageId);
$request->setAlias(
Mage_Core_Model_Url_Rewrite::REWRITE_REQUEST_PATH_ALIAS,
$identifier
);
return true;
If there was no matching $pageId
, we return false
and allow the next router in the system to have its chance. However, if we did find a match, we set a module name, controller name and action name, as well as a request parameter for the page ID, and then return true.
All of which seems a little premature. We’ve indicated there’s a match by returning true, but we haven’t done anything to produce page output. What gives?
If you’re stumped don’t be ashamed, because it’s a little tricky and counter intuitive. While we did return true
, nothing set the controller as dispatched. So, back up in our front controller’s foreach
loop
File: app/code/core/Mage/Core/Controller/Varien/Front.php
while (!$request->isDispatched() && $i++<100) {
foreach ($this->_routers as $router) {
if ($router->match($this->getRequest())) {
break;
}
}
}
we bail/break
on the foreach
loop because of the match, but because the Cms
router never marked the request as dispatched, we stay in the while
loop and start trying to match on ALL the routers again. This means after the first CMS match
is called, we call match on the Admin
router, and then the Standard
router.
You may be thinking to yourself
If they didn’t match before, won’t we just reach the
Cms
router again?
It’s a little confounding until you remember back to the epic standard router match
method, and calls like this
#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
if ($request->getModuleName()) {
$module = $request->getModuleName();
...
if ($request->getControllerName()) {
$controller = $request->getControllerName();
...
if ($request->getActionName()) {
$action = $request->getActionName();
At the time they seemed like useless noise, but now we have enough information to realize why they’re there. The Cms
router’s match method altered the request object, and set these very properties, which will allow the Standard
router to match the second time through. That means a URL like this
http://magento.example.com/about-magento-demo-store
will actually be dispatched by the Standard
router as though it was called with a URL whose path information was
cms/page/view/page_id/3
which means that all non-home CMS page requests are handled by the viewAction
method of Mage_Cms_PageController
#File: app/code/core/Mage/Cms/controllers/PageController.php
class Mage_Cms_PageController extends Mage_Core_Controller_Front_Action
{
public function viewAction()
{
$pageId = $this->getRequest()
->getParam('page_id', $this->getRequest()->getParam('id', false));
if (!Mage::helper('cms/page')->renderPage($this, $pageId)) {
$this->_forward('noRoute');
}
}
}
This action method passes the page id onto the cms/page
helper, which ultimately renders the page. (If you’re interested in the actual rendering process, No Frills Magento Layout covers that, and much more)
CMS Wrap Up
After looking at the Cms
router object, it becomes clear that our original assessment of what a Router
object should do
- Provide a
match
method which examines the request object and returnstrue
if the router wishes to “claim”
a request and stop other router objects from acting - Mark the request object as dispatched, or through inaction fail to mark it as dispatched
-
Set the body/contents of the request object, either directly or via another Magento system
is flawed. The CMS router does some, but not all, of this. Instead of kicking off a system to produce output, it jiggers the request so it can be interpreted again by the Standard
router. Of course, if that’s the real correct usage, why wasn’t the Admin
router built the same way? We ask not because there’s a right answer, but to stress that these sorts of questions are the ultimate undoing of many configuration based, deeply abstract, systems. Without a clear, firm hand guiding development and guiding usage, systems become a tangled mess of code that works, but lacks any central vision and becomes difficult for outsiders to follow.
The Default Router Object
This brings us to our final object, the Default
router, defined by the class Mage_Core_Controller_Varien_Router_Default
. The Default
router is added last, and is there to signal the rest of the system that the URL/request has no MVC, CMS, or third party route match.
Compared to the others, the match
method here is relatively straight forward. First, we derive a default module, controller, and action name from the web/default/no_route
config node
#File: app/code/core/Mage/Core/Controller/Varien/Router/Default.php
public function match(Zend_Controller_Request_Http $request)
{
$noRoute = explode('/', Mage::app()->getStore()->getConfig('web/default/no_route'));
$moduleName = isset($noRoute[0]) ? $noRoute[0] : 'core';
$controllerName = isset($noRoute[1]) ? $noRoute[1] : 'index';
$actionName = isset($noRoute[2]) ? $noRoute[2] : 'index';
..
}
If there’s no value set for the config node, or the value set is only a partial path, the hard coded defaults of core
, index
, and index
are used.
The configuration node web/default/no_route
corresponds to the system configuration pane at
Admin -> System -> Configuration -> Web -> Default Pages -> Default No-route URL
In a stock system, this value is set to
cms/index/noRoute
meaning most Magento 404 pages will be routed to the noRouteAction
method of Mage_Cms_IndexController
.
Back to the match
method, next we have another special case for the admin console
#File: app/code/core/Mage/Core/Controller/Varien/Router/Default.php
if (Mage::app()->getStore()->isAdmin()) {
$adminFrontName = (string)Mage::getConfig()->getNode('admin/routers/adminhtml/args/frontName');
if ($adminFrontName != $moduleName) {
$moduleName = 'core';
$controllerName = 'index';
$actionName = 'noRoute';
Mage::app()->setCurrentStore(Mage::app()->getDefaultStoreView());
}
}
The exact edge case this code handles is unclear. It looks as though it’s there to ensure if the store is set to Admin mode and our derived module name doesn’t match the current front name configured for the admin module, that we unset admin mode and pass the user to the Mage_Core
module’s index controller’s noRoute
action. We’ve already talked a bit about the weirdness of the admin 404 process, this would appear to be another example of that. (If anyone has information about the intent of this code, please get in touch)
Finally, similar to the Cms
router, we set these values on the request object and return true
without marking the request as dispatched.
#File: app/code/core/Mage/Core/Controller/Varien/Router/Default.php
$request->setModuleName($moduleName)
->setControllerName($controllerName)
->setActionName($actionName);
return true;
Just as with the Cms
router, this will signal the front controller to send the request through the router objects again, allowing the existing Magento systems to handle a noRoute situation. If you’re interested in the various ways this can happen, it’s worth reading the Magento’s Many 404 Pages article.
Router Wrap Up
That, in a nutshell, is Magento’s routing system. As you’ve can see, there’s a lot of lingering code and patterns still kicking around for legacy reasons, and this legacy behavior can often make it difficult to suss out the true intentions of the system. Based on news from the MDP conference this year, and small steps taken in the final 1.6 CE release, it appears the core team is slowly cleaning up these legacy system in preparation for the Magento 2 refactoring release. Hope springs eternal.
For people starting out with Magento, I’d say best practices are to stay out of this system. Learn how to setup CMS pages and MVC controllers for the front and backend, and then just treat the code as a black box. A key to working with every software system is to trust that the sub-systems will do their job, or at least accept that they’re going to do what they’re going to do, and focus on writing code to achieve your own goals. When you’re ready to dive a little deeper, I hope these guides can help you explore the cavernous depths.
Next time we’ll be wrapping up the series by exploring the system with one of the higher usage/misunderstanding ratio, the rewrite system. While not technically part of the routing system, Magento’s two request rewrite systems impact the request object that the router objects deal with, and therefore no discussion of routing is complete without diving in.
If you’ve found any of this useful, you’ll find Pulse Storm’s Magento products equally useful and time saving. If your retail senses don’t need stimulating at the moment, please consider donating a few bucks to the continued development of these tutorials.