diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index 990a360..0ef2d9d 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -3,6 +3,8 @@
 use Symfony\Component\ClassLoader\UniversalClassLoader;
 use Symfony\Component\ClassLoader\ApcUniversalClassLoader;
 
+use Drupal\Core\Request;
+
 /**
  * @file
  * Functions that need to be loaded on every Drupal request.
@@ -718,13 +720,6 @@ function drupal_environment_initialize() {
     $_SERVER['HTTP_HOST'] = '';
   }
 
-  // When clean URLs are enabled, emulate ?q=foo/bar using REQUEST_URI. It is
-  // not possible to append the query string using mod_rewrite without the B
-  // flag (this was added in Apache 2.2.8), because mod_rewrite unescapes the
-  // path before passing it on to PHP. This is a problem when the path contains
-  // e.g. "&" or "%" that have special meanings in URLs and must be encoded.
-  $_GET['q'] = request_path();
-
   // Enforce E_STRICT, but allow users to set levels not part of E_STRICT.
   error_reporting(E_STRICT | E_ALL | error_reporting());
 
@@ -2715,157 +2710,59 @@ function language_default() {
 }
 
 /**
+ * Returns the request object for the current request.
+ *
+ * @return Drupal\Core\Request
+ *   The request object for this request, as populated from the PHP superglobals.
+ */
+function request() {
+  $request = &drupal_static(__FUNCTION__);
+  if (empty($request)) {
+    $request = Request::createFromGlobals();
+  }
+  return $request;
+}
+
+/**
  * Returns the requested URL path of the page being viewed.
  *
- * Examples:
- * - http://example.com/node/306 returns "node/306".
- * - http://example.com/drupalfolder/node/306 returns "node/306" while
- *   base_path() returns "/drupalfolder/".
- * - http://example.com/path/alias (which is a path alias for node/306) returns
- *   "path/alias" as opposed to the internal path.
- * - http://example.com/index.php returns an empty string (meaning: front page).
- * - http://example.com/index.php?page=1 returns an empty string.
+ * @deprecated
  *
  * @return
  *   The requested Drupal URL path.
  *
- * @see current_path()
+ * @see Drupal\Core\Request::requestPath()
  */
 function request_path() {
-  static $path;
-
-  if (isset($path)) {
-    return $path;
-  }
-
-  if (isset($_GET['q'])) {
-    // This is a request with a ?q=foo/bar query string. $_GET['q'] is
-    // overwritten in drupal_path_initialize(), but request_path() is called
-    // very early in the bootstrap process, so the original value is saved in
-    // $path and returned in later calls.
-    $path = $_GET['q'];
-  }
-  elseif (isset($_SERVER['REQUEST_URI'])) {
-    // This request is either a clean URL, or 'index.php', or nonsense.
-    // Extract the path from REQUEST_URI.
-    $request_path = strtok($_SERVER['REQUEST_URI'], '?');
-    $base_path_len = strlen(rtrim(dirname($_SERVER['SCRIPT_NAME']), '\/'));
-    // Unescape and strip $base_path prefix, leaving q without a leading slash.
-    $path = substr(urldecode($request_path), $base_path_len + 1);
-    // If the path equals the script filename, either because 'index.php' was
-    // explicitly provided in the URL, or because the server added it to
-    // $_SERVER['REQUEST_URI'] even when it wasn't provided in the URL (some
-    // versions of Microsoft IIS do this), the front page should be served.
-    if ($path == basename($_SERVER['PHP_SELF'])) {
-      $path = '';
-    }
-  }
-  else {
-    // This is the front page.
-    $path = '';
-  }
-
-  // Under certain conditions Apache's RewriteRule directive prepends the value
-  // assigned to $_GET['q'] with a slash. Moreover we can always have a trailing
-  // slash in place, hence we need to normalize $_GET['q'].
-  $path = trim($path, '/');
-
-  return $path;
+  return request()->requestPath();
 }
 
 /**
  * Returns a component of the current Drupal path.
  *
- * When viewing a page at the path "admin/structure/types", for example, arg(0)
- * returns "admin", arg(1) returns "structure", and arg(2) returns "types".
- *
- * Avoid use of this function where possible, as resulting code is hard to
- * read. In menu callback functions, attempt to use named arguments. See the
- * explanation in menu.inc for how to construct callbacks that take arguments.
- * When attempting to use this function to load an element from the current
- * path, e.g. loading the node on a node page, use menu_get_object() instead.
- *
- * @param $index
- *   The index of the component, where each component is separated by a '/'
- *   (forward-slash), and where the first component has an index of 0 (zero).
- * @param $path
- *   A path to break into components. Defaults to the path of the current page.
+ * @deprecated
  *
  * @return
  *   The component specified by $index, or NULL if the specified component was
- *   not found. If called without arguments, it returns an array containing all
- *   the components of the current path.
+ *   not found.
  */
 function arg($index = NULL, $path = NULL) {
-  // Even though $arguments doesn't need to be resettable for any functional
-  // reasons (the result of explode() does not depend on any run-time
-  // information), it should be resettable anyway in case a module needs to
-  // free up the memory used by it.
-  // Use the advanced drupal_static() pattern, since this is called very often.
-  static $drupal_static_fast;
-  if (!isset($drupal_static_fast)) {
-    $drupal_static_fast['arguments'] = &drupal_static(__FUNCTION__);
-  }
-  $arguments = &$drupal_static_fast['arguments'];
-
-  if (!isset($path)) {
-    $path = $_GET['q'];
-  }
-  if (!isset($arguments[$path])) {
-    $arguments[$path] = explode('/', $path);
-  }
-  if (!isset($index)) {
-    return $arguments[$path];
-  }
-  if (isset($arguments[$path][$index])) {
-    return $arguments[$path][$index];
-  }
+  return request()->pathElement($index);
 }
 
 /**
  * Returns the IP address of the client machine.
  *
- * If Drupal is behind a reverse proxy, we use the X-Forwarded-For header
- * instead of $_SERVER['REMOTE_ADDR'], which would be the IP address of
- * the proxy server, and not the client's. The actual header name can be
- * configured by the reverse_proxy_header variable.
+ * @deprecated
  *
  * @return
  *   IP address of client machine, adjusted for reverse proxy and/or cluster
  *   environments.
+ *
+ * @see Symfony\Component\HttpFoundation\Request::getClientIp()
  */
 function ip_address() {
-  $ip_address = &drupal_static(__FUNCTION__);
-
-  if (!isset($ip_address)) {
-    $ip_address = $_SERVER['REMOTE_ADDR'];
-
-    if (variable_get('reverse_proxy', 0)) {
-      $reverse_proxy_header = variable_get('reverse_proxy_header', 'HTTP_X_FORWARDED_FOR');
-      if (!empty($_SERVER[$reverse_proxy_header])) {
-        // If an array of known reverse proxy IPs is provided, then trust
-        // the XFF header if request really comes from one of them.
-        $reverse_proxy_addresses = variable_get('reverse_proxy_addresses', array());
-
-        // Turn XFF header into an array.
-        $forwarded = explode(',', $_SERVER[$reverse_proxy_header]);
-
-        // Trim the forwarded IPs; they may have been delimited by commas and spaces.
-        $forwarded = array_map('trim', $forwarded);
-
-        // Tack direct client IP onto end of forwarded array.
-        $forwarded[] = $ip_address;
-
-        // Eliminate all trusted IPs.
-        $untrusted = array_diff($forwarded, $reverse_proxy_addresses);
-
-        // The right-most IP is the most specific we can trust.
-        $ip_address = array_pop($untrusted);
-      }
-    }
-  }
-
-  return $ip_address;
+  return request()->getClientIp(variable_get('reverse_proxy', 0));
 }
 
 /**
diff --git a/core/includes/menu.inc b/core/includes/menu.inc
index 40f9bfe..d0a7c88 100644
--- a/core/includes/menu.inc
+++ b/core/includes/menu.inc
@@ -445,7 +445,7 @@ function menu_get_item($path = NULL, $router_item = NULL) {
     if (variable_get('menu_rebuild_needed', FALSE) || !variable_get('menu_masks', array())) {
       menu_rebuild();
     }
-    $original_map = arg(NULL, $path);
+    $original_map = explode('/', $path);
 
     // Since there is no limit to the length of $path, use a hash to keep it
     // short yet unique.
@@ -1705,7 +1705,7 @@ function menu_get_active_help() {
     return '';
   }
 
-  $arg = drupal_help_arg(arg(NULL));
+  $arg = drupal_help_arg(explode('/', request()->systemPath()));
 
   foreach (module_implements('help') as $module) {
     $function = $module . '_help';
diff --git a/core/includes/path.inc b/core/includes/path.inc
index 44bf3fe..0969d51 100644
--- a/core/includes/path.inc
+++ b/core/includes/path.inc
@@ -292,7 +292,7 @@ function drupal_is_front_page() {
   if (!isset($is_front_page)) {
     // As drupal_path_initialize updates $_GET['q'] with the 'site_frontpage' path,
     // we can check it against the 'site_frontpage' variable.
-    $is_front_page = ($_GET['q'] == variable_get('site_frontpage', 'user'));
+    $is_front_page = (request()->get('q') == variable_get('site_frontpage', 'user'));
   }
 
   return $is_front_page;
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 40c4f35..9983a18 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -2332,7 +2332,7 @@ function template_preprocess_html(&$variables) {
   }
 
   // Populate the body classes.
-  if ($suggestions = theme_get_suggestions(arg(), 'page', '-')) {
+  if ($suggestions = theme_get_suggestions(request()->pathElements(), 'page', '-')) {
     foreach ($suggestions as $suggestion) {
       if ($suggestion != 'page-front') {
         // Add current suggestion to page classes to make it possible to theme
@@ -2380,7 +2380,7 @@ function template_preprocess_html(&$variables) {
   $variables['head_title'] = implode(' | ', $head_title);
 
   // Populate the page template suggestions.
-  if ($suggestions = theme_get_suggestions(arg(), 'html')) {
+  if ($suggestions = theme_get_suggestions(request()->pathElements(), 'html')) {
     $variables['theme_hook_suggestions'] = $suggestions;
   }
 }
@@ -2392,9 +2392,6 @@ function template_preprocess_html(&$variables) {
  * inside "modules/system/page.tpl.php". Look in there for the full list of
  * variables.
  *
- * Uses the arg() function to generate a series of page template suggestions
- * based on the current path.
- *
  * Any changes to variables in this preprocessor should also be changed inside
  * template_preprocess_maintenance_page() to keep all of them consistent.
  *
@@ -2439,7 +2436,7 @@ function template_preprocess_page(&$variables) {
   }
 
   // Populate the page template suggestions.
-  if ($suggestions = theme_get_suggestions(arg(), 'page')) {
+  if ($suggestions = theme_get_suggestions(request()->pathElements(), 'page')) {
     $variables['theme_hook_suggestions'] = $suggestions;
   }
 }
@@ -2508,7 +2505,7 @@ function template_process_html(&$variables) {
  * base the additional suggestions on the path of the current page.
  *
  * @param $args
- *   An array of path arguments, such as from function arg().
+ *   An array of path arguments.
  * @param $base
  *   A string identifying the base 'thing' from which more specific suggestions
  *   are derived. For example, 'page' or 'html'.
diff --git a/core/lib/Drupal/Core/Request.php b/core/lib/Drupal/Core/Request.php
new file mode 100644
index 0000000..d514766
--- /dev/null
+++ b/core/lib/Drupal/Core/Request.php
@@ -0,0 +1,154 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\Request.
+ */
+
+namespace Drupal\Core;
+
+use Symfony\Component\HttpFoundation\Request as HttpFoundationRequest;
+
+/**
+ * Description of DrupalRequest
+ */
+class Request extends HttpFoundationRequest {
+
+  /**
+   * An array of elements in the path, which are arguments to the request.
+   *
+   * @var array
+   */
+  protected $pathElements;
+
+  /**
+   * The requested URL path of the page being viewed
+   *
+   * @var string
+   */
+  protected $requestPath;
+
+  /**
+   * Returns a component of the current path.
+   *
+   * When viewing a page at the path "admin/structure/types", for example,
+   * index 0 returns "admin", index 1 returns "structure", and index 2 returns
+   * "types".
+   *
+   * Use of this method is discouraged, as it results in a code that is
+   * hard-coded to a particular path.  Generally it is bad practice to assume
+   * that a particular piece of code will run on a particular path.
+   *
+   * @param $index
+   *   The index of the component, where each component is separated by a '/'
+   *   (forward-slash), and where the first component has an index of 0 (zero).
+   *
+   * @return
+   *   The component specified by $index, or NULL if the specified component was
+   *   not found.
+   */
+  public function pathElement($index) {
+    $elements = $this->pathElements();
+    return isset($elements[$index]) ? $elements[$index] : NULL;
+  }
+
+  /**
+   * Returns the current path broken up into an array.
+   *
+   * @return array
+   *   An array representing the elements of the path.
+   */
+  public function pathElements() {
+    if (empty($this->pathElements)) {
+      $this->pathElements = explode('/', $this->systemPath());
+    }
+    return $this->pathElements;
+  }
+
+  /**
+   * Returns the requested URL path of the page being viewed.
+   *
+   * Examples:
+   * - http://example.com/node/306 returns "node/306".
+   * - http://example.com/drupalfolder/node/306 returns "node/306" while
+   *   base_path() returns "/drupalfolder/".
+   * - http://example.com/path/alias (which is a path alias for node/306) returns
+   *   "path/alias" as opposed to the internal path.
+   * - http://example.com/index.php returns an empty string (meaning: front page).
+   * - http://example.com/index.php?page=1 returns an empty string.
+   *
+   * @return
+   *   The requested URL path, as it came from the user agent.
+   */
+  public function requestPath() {
+    if (empty($this->requestPath)) {
+      $raw_path = '';
+
+      $q = $this->query->get('q');
+
+      if (!empty($q)) {
+        // This is a request with a ?q=foo/bar query string. That trumps all other
+        // path locations.
+        $raw_path = $q;
+      }
+      else {
+        // This request is either a clean URL, or 'index.php', or nonsense.
+        // Extract the path from REQUEST_URI.
+        $request_uri = $this->getRequestUri();
+        $request_path = strtok($request_uri, '?');
+        $script_name = $this->getScriptName();
+        $base_path_len = strlen(rtrim(dirname($script_name), '\/'));
+        // Unescape and strip $base_path prefix, leaving q without a leading slash.
+        $raw_path = substr(urldecode($request_path), $base_path_len + 1);
+        // If the path equals the script filename, either because 'index.php' was
+        // explicitly provided in the URL, or because the server added it to
+        // $_SERVER['REQUEST_URI'] even when it wasn't provided in the URL (some
+        // versions of Microsoft IIS do this), the front page should be served.
+        $php_self = $this->server->get('PHP_SELF');
+        if ($raw_path == basename($php_self)) {
+          $raw_path = '';
+        }
+      }
+
+      // Under certain conditions Apache's RewriteRule directive prepends the value
+      // assigned to $_GET['q'] with a slash. Moreover we can always have a trailing
+      // slash in place, hence we need to normalize $_GET['q'].
+      $this->requestPath = trim($raw_path, '/');
+    }
+
+    return $this->requestPath;
+  }
+
+  /**
+   * Return the current URL path of the page being viewed.
+   *
+   * Examples:
+   * - http://example.com/node/306 returns "node/306".
+   * - http://example.com/drupalfolder/node/306 returns "node/306" while
+   *   base_path() returns "/drupalfolder/".
+   * - http://example.com/path/alias (which is a path alias for node/306) returns
+   *   "node/306" as opposed to the path alias.
+   *
+   * This function is not available in hook_boot() so use Request::requestPath()
+   * instead.  Be aware that requestPath() does not have aliases resolved.
+   *
+   * @return
+   *   The current URL path, with aliases resolved.
+   */
+  public function systemPath() {
+    if (empty($this->systemPath)) {
+      // @todo Temporary hack. Fix when path is an object.
+      require_once DRUPAL_ROOT . '/core/includes/path.inc';
+
+      $path = $this->requestPath();
+
+      if (empty($path)) {
+        // @todo Temporary hack. Fix when configuration is injectable.
+        $path = variable_get('site_frontpage', 'node');
+      }
+      $this->systemPath = drupal_get_normal_path($path);
+    }
+
+    return $this->systemPath;
+  }
+}
diff --git a/core/modules/simpletest/simpletest.info b/core/modules/simpletest/simpletest.info
index bbe65f0..8faf836 100644
--- a/core/modules/simpletest/simpletest.info
+++ b/core/modules/simpletest/simpletest.info
@@ -29,6 +29,7 @@ files[] = tests/module.test
 files[] = tests/password.test
 files[] = tests/path.test
 files[] = tests/registry.test
+files[] = tests/request.test
 files[] = tests/schema.test
 files[] = tests/session.test
 files[] = tests/symfony.test
diff --git a/core/modules/simpletest/tests/form_test.module b/core/modules/simpletest/tests/form_test.module
index e1e2435..28ba566 100644
--- a/core/modules/simpletest/tests/form_test.module
+++ b/core/modules/simpletest/tests/form_test.module
@@ -1526,7 +1526,7 @@ function form_test_clicked_button($form, &$form_state) {
   // 'image_button', and a 'button' with #access=FALSE. This enables form.test
   // to test a variety of combinations.
   $i=0;
-  $args = array_slice(arg(), 2);
+  $args = array_slice(request()->pathElements(), 2);
   foreach ($args as $arg) {
     $name = 'button' . ++$i;
     // 's', 'b', or 'i' in the argument define the button type wanted.
diff --git a/core/modules/simpletest/tests/request.test b/core/modules/simpletest/tests/request.test
new file mode 100644
index 0000000..93e215d
--- /dev/null
+++ b/core/modules/simpletest/tests/request.test
@@ -0,0 +1,92 @@
+<?php
+
+/**
+ * @file
+ * Tests for the request object.
+ */
+
+use Drupal\Core\Request;
+
+/**
+ * Unit tests for the Request object.
+ */
+class RequestUnitTest extends DrupalUnitTestCase {
+  protected $testPath = 'foo/bar';
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Request',
+      'description' => 'Tests for request information',
+      'group' => 'Request',
+    );
+  }
+
+  public function setUp() {
+    parent::setUp();
+
+    require_once DRUPAL_ROOT . '/core/lib/Drupal/Core/Request.php';
+
+    // This unfortunately necessary because Drupal's registry throws a database
+    // exception when testing class_exist on non-existent classes in unit tests.
+    // This is the point of one of our tests so we have to remove the registry
+    // prior to running our tests.
+    spl_autoload_unregister('drupal_autoload_class');
+    spl_autoload_unregister('drupal_autoload_interface');
+  }
+
+  public function tearDown() {
+    // Re-register drupal's autoload classes. See setUp for the reasoning.
+    spl_autoload_register('drupal_autoload_class');
+    spl_autoload_register('drupal_autoload_interface');
+
+    parent::tearDown();
+  }
+
+  /**
+   * Test the request path logic.
+   */
+  function testRequestPathFromPath() {
+    $request = Request::create('/' . $this->testPath);
+
+    $this->assertEqual($request->requestPath(), $this->testPath, t('Correct request path derived from path.'));
+  }
+
+  /**
+   * Test the request path logic.
+   */
+  function testRequestPathFromQuery() {
+    $request = Request::create('index.php', 'GET', array('q' => $this->testPath));
+
+    $this->assertEqual($request->requestPath(), $this->testPath, t('Correct request path derived from arguments.'));
+  }
+
+  /**
+   * Test the path fragment logic.
+   */
+  function testPathFragment() {
+    $request = Request::create('/' . $this->testPath);
+
+    $this->assertEqual($request->pathElement(0), 'foo', t('Correct first path element returned.'));
+    $this->assertEqual($request->pathElement(1), 'bar', t('Correct second path element returned.'));
+    $this->assertNull($request->pathElement(2), t('Null returned for non-existent path element.'));
+  }
+
+  /**
+   * Test the path fragment logic.
+   */
+  function testPathFragments() {
+    $request = Request::create('/' . $this->testPath);
+
+    $elements = $request->pathElements();
+
+    $this->assertEqual($elements[0], 'foo', t('Correct first path element returned.'));
+    $this->assertEqual($elements[1], 'bar', t('Correct second path element returned.'));
+    $this->assertTrue(empty($elements[2]), t('Null returned for non-existent path element.'));
+  }
+
+
+  // System path cannot be unit tested, because it relies on the database.
+  // @TODO: Add a unit test here once the path logic becomes an object and can
+  // be mocked.
+
+}
