Overview

Router Structure

The router takes a request as input, matches it against a predefined collection of routes, checks its validity and calls the corresponding function which returns a Response object for rendering.

URL matching is done by the Symfony routing component, which has been part of Submitty since v.19.06.01.

Validity checking checks if the user is authorized to access certain parts of Submitty. For example, a user who is not logged in is not allowed to go anywhere else but login page. Also, a student will be prevented from accessing pages only accessible to instructors. If validity checking is not passed, the user will be redirected to appropriate pages or, for API calls, get JSON responses with fail status.

Function call is a process that calls the proper function. All parameters in the URL will be passed into the functions if there are corresponding parameter definitions in function signatures. The function returns a Response which carries necessary information to display.

Usage

While a complete instruction on usage can be found at Symfony Documentation. Here, several examples at Submitty will be given to help developers set up routes quickly.

It is assumed that use Symfony\Component\Routing\Annotation\Route is contained in the header of the file.

Route without Course Information

While most routes in Submitty are specific to one course, there are places that do not need course information like login page or home page.

For those routes, it is simple to set up a route.

/**
 * @Route("/home")
 */
public function showHomepage() {...}

Route with Course Information

A majority of links in Submitty require course information to return correct contents. Therefore, it is necessary for the router to know which (term, course) tuple is requested. It is needed to prepend the route with /courses/{_term}/{_course}. The course information will be loaded automatically before calling the function.

/**
 * @Route("/courses/{_term}/{_course}/reports")
 */
public function showReportPage() {...}

To visit the page defined above, simply go to {base_url}/{current term}/sample/reports.

Route with Parameters

Sometimes we may want to pass parameters to functions. Wrapping the parameter name with brackets will do the trick.

/**
 * @Route("/courses/{_term}/{_course}/student/{gradeable_id}")
 */
public function showHomeworkPage($gradeable_id){...}

Note that {_term} and {_course} are actually special cases of parameters that are automatically processed by the router.

Also, note that parameters in the GET query are passed to the function too without the need for explicit definition as long as parameter names are the same.

/**
 * @Route("/authentication/login")
 */
public function loginForm($old = null) {...}

The value of $old will be set to Submitty if you go to /authentication/login?old=submitty.

Route with Parameter Requirements

In some cases, you may want routes to match number-only parameters, or those not starting with certain words. This could be done by adding a requirements field in the route definition.

/**
 * @Route("/courses/{_term}/{_course}/notifications/{nid}", requirements={"nid": "[1-9]\d*"})
 */
public function openNotification($nid) {...}
/**
 * @Route("/courses/{_term}/{_course}", requirements={"_term": "^(?!api)[^\/]+", "_course": "[^\/]+"})
 */
public function navigationPage() {...}

Route that Uses POST

To prevent cross-site request forgery, all POST requests (except those related to authentication) are forced to pass CSRF checks. While the router can automatically detect POST requests by seeing if $_POST is empty, it is recommended to explicitly state that the route accepts only POST access considering that $_POST may be empty for POST requests.

Note that, by default, the methods of routes are set to be ALL, which means the route accepts all kind of requests. Therefore, in some cases one route may be masked by another. If you have two routes with the same name, please specify the methods of both.

/**
 * @Route("/home/change_username", methods={"POST"})
 */
public function changeUserName(){...}

Route that Needs Access Control

Access control can be enforced easily via @AccessControl annotation. Please add use app\libraries\routers\AccessControl to the file before using it.

For example, the following route will only allow instructor access.

/**
 * @Route("/courses/{_term}/{_course}/course_materials/modify_permission")
 * @AccessControl(role="INSTRUCTOR")
 */
public function modifyCourseMaterialsFilePermission($filename, $checked)

For more examples about @AccessControl, please read the documentation in the code.

Route for API

To create a route for the Submitty API, the only difference from above is prepending /api to the route definition. You can define several routes for the same method, generally having one API route and one Web route. Any route that uses the /api goes through a slightly different authentication mechanism involving JSON Web Tokens.

For example, the following route is the API for list of courses:

/**
 * @Route("/home/courses/new", methods={"POST"})
 * @Route("/api/courses", methods={"POST"})
 */
public function createCourse()

And an API route for a method within a course:

/**
 * @Route("/courses/{_term}/{_course}/users", methods={"GET"})
 * @Route("/api/courses/{_term}/{_course}/users", methods={"GET"})
 */
public function getStudents()

Any API endpoint must either return a JsonResponse or a RedirectResponse (see below.

FAQ

Here are some frequently asked questions regarding the router.

What is the ResponseInterface object? Is it required?

It is not required to return a concrete ResponseInterface (e.g. JsonResponse) object.

The ResponseInterface object is used for handling the various types of responses, be it a web response showing a page, JSON response, a redirect, or some combination of all three. When building an API for Submitty, we envision that some functions can be reused and return different types of data depending on if it is an API call.

If you wish to have a Response that can be some combination of other base Response types, use the MultiResponse object, which can have three optional parts of Responses: WebResponse, JsonResponse, and RedirectResponse. When a method returns the MultiResponse object, it will render the appropriate part depending on the context. If there exists only one component, say there is only JsonResponse, then render it. If there are multiple components, say JsonResponse and WebResponse, then render JsonResponse for API calls and WebResponse for browser access.

As API is still a work in progress, more detailed documentation on Responses will be written as it matures. For now, considering the efforts needed to refactor all the code and the limited benefits it brings for most methods, it is optional to return a Response object. Backward compatibility is ensured.

Are there any naming conventions for URLs?

It is recommended to use snake_case for URLs consisting of multiple words.

How to construct URLs?

For PHP, use Core::buildUrl or Core::buildCourseUrl.

For JavaScript, use buildUrl or buildCourseUrl.

Core::buildCourseUrl in PHP and buildCourseUrl in Javascript will prepend course information to the URL (e.g. /f19/sample).