MVC
Model-View-Controller
What is the MVC pattern?
The MVC pattern is an architectural design pattern generally used in OOP. The MVC pattern is composed of three main distinct and independent, yet interconnected, elements:
- Model: handles data management, is usually connected to a database, and each model instance represents a record in a table;
- View: receives data from the model and handles its graphical representation. The view is the component that interfaces directly with the user and with which they interact;
- Controller: handles the actual logic of the software. In other words, it's where the magic happens;
A simple example to put the MVC pattern into practice
Let's assume we have a form to register reviews. We declare three routes:
/review-formto display the form;/review-form/storeto process the form submission;/review-form/thank-youto display a thank you page;
router()->get("/review-form", [\App\Modules\Reviews\Reviews::class, "review_form"])->name("reviews.form");
router()->post("/review-form/store", [\App\Modules\Reviews\Reviews::class, "review_form_store"])->name("review_form.store");
router()->get("/review-form/thank-you", [\App\Modules\Reviews\Reviews::class, "review_form_thank_you"])->name("review_form.thank_you");
//or with routes grouping
router()->group("review-form", function($group) {
$group->get("/", [\App\Modules\Reviews\Reviews::class, "review_form"])->name("reviews.form");
$group->post("/store", [\App\Modules\Reviews\Reviews::class, "review_form_store"])->name("review_form.store");
$group->get("/thank-you", [\App\Modules\Reviews\Reviews::class, "review_form_thank_you"])->name("review_form.thank_you");
});
Visiting the addresses mapped in the routes will invoke the corresponding methods of the Reviews controller.
- Visiting
/review-formwill invoke thereview_form()method, which will display the form; /review-form/storeis the address for submitting the form via POST method and will invoke thereview_form_store()method;- Visiting
/review-form/thank-youwill invoke thereview_form_thank_you()method, which will display a thank you page;
Model
A model is represented by a class that extends \App\System\MVC\Model, which in turn extends the \Illuminate\Database\Eloquent\Model class. For more information, refer to the database section where links to the official Eloquent ORM documentation are provided.
<?php
namespace App\Modules\Reviews;
class Review extends \App\System\MVC\Model {
// Sets the model's table (optional)
protected $table = "reviews";
}
View(s)
Views represent the point of contact between the software and the end user. They represent data in a comprehensible manner and enable interaction with the application. We'll soon look in detail at how to build a view and its main functions.
<h1><?=$title?></h1>
<form action="<?=$action?>" method="POST">
<?=csrf_input()?> <!-- prints <input type="hidden" name="csrf_token" value="**CSRF TOKEN**"> -->
<div>
<label>Evaluation</label>
<input type="range" min="0" max="5" step="1" name="evaluation" required>
</div>
<div>
<label>Comment</label>
<textarea name="comment" rows="10" required></textarea>
</div>
<div>
<button type="submit">Send</button>
</div>
</form>
Controller
When a route is declared, the request can be handled through a closure or through the methods of a controller. As mentioned, the controller handles requests and software logic. A controller is represented by a class that extends \App\System\MVC\Controller. The following code is an example of a controller.
<?php
namespace App\Modules\Reviews;
use \App\System\Http\ServerRequest;
class Reviews extends \App\System\MVC\Controller {
public function review_form() {
return module_view("Reviews::form", [
"title" => "Review form",
"action" => url("review_form.store")
]);
}
public function review_form_store(ServerRequest $request) {
$v = validator($request->all(), [
"evaluation" => ["required", "integer"],
"comment" => ["required", "string"],
]);
//aborts the script with a 400 error if validator fails
abort_if(!$v->success(), 400);
$review = new Review();
$review->evaluation = $request->input("evaluation");
$review->comment = $request->input("comment");
//aborts the script with a 500 error if a save error occurs
abort_if(!$review->save(), 500);
return redirect(url("review_form.thank_you"));
}
public function review_form_thank_you() {
return module_view("Reviews::thank_you", [
"text" => "Thank you for your review!"
]);
}
}
review_form(): will be invoked by visiting the relative URL via the GET method. The module_view(...) function is a shortcut for using a view contained within the module. We will see how to create and use views later, while for the functioning of modules, please refer to the dedicated section. The first parameter is the name of the module, the second is the name of the view (it can also be a relative path in the case of subfolders, e.g., forms/new_review), and the third is an array containing the parameters to pass to the view. In this case, a title and the address to submit the form are passed. url(...) takes the name of a route as its first argument, any URL parameters as its second argument, and returns the string representing the associated URL. Before you can use the url(...) function, you must assign a name to the desired route.
Also very similar is review_form_thank_you().
Example of using the url() function:
router()->get("/posts", [\App\Modules\MyModule\MyController::class, "my_method"])->name("posts.list");
url("route.custom_name"); // https://mydomain.com/posts
router()->get("/posts/{category}/{id}", [\App\Modules\MyModule\MyController::class, "my_method"])->name("posts.show");
url("route.custom_name", ["category" => "frameworks", "id" => 1234]); // https://mydomain.com/posts/frameworks/1234
review_form_store(): This is called when the form is submitted via the POST method. First, the request is validated using the validator(...) function. If the request is not valid, execution is aborted and the server responds with an HTTP 400 Bad Request error using the abort_if(...) function. If the request is valid, a new Review object is instantiated and populated, and then saved to the database. In the end, the request is redirected to the thank you page.
Views
- Searching for a view
- Views locations
- Creating and using a view
- Checking if a view exists
- Rendering a view
- Extend a view
- Modules views
- The Document class, styles and scripts
- Style sections
- Opt-* attributes
- Script sections
- Printing style and script sections
Monti implements a native PHP template engine.
Searching for a View
You can search for a view using the various methods of the \App\System\MVC\View\View class (hereafter just View for simplicity) or via various helpers. Views are normal .php files, however, when searching for a view, you must use the file name without the extension.
Each time a view is used, two arguments are passed:
- The name of the view (without the
.phpextension). Optionally, a view name may contain a namespace; - An array containing the parameters to pass to the view (only necessary if the view requires parameters);
//View from absolute path
//You must specify the full path to the file containing the view (without the .php extension)
$view = new View(__DIR__ . "/absolute/path/to/view", **array_of_view_arguments**);
//View from relative path
//You must specify the relative path to the file containing the view (without the .php extension)
//Search for a view based on the paths in the views.paths.main configuration file.
View::get("view_name", [** optional arguments**]);
view("view_name", [** optional arguments**]);
view("path/to_another/folder/view_name", [** optional arguments**]);
//Search for a module view.
View::module("ModuleNamespace::view_name", [** optional arguments**]);
module_view("ModuleNamespace::view_name", [** optional arguments**]);
module_view("path/to_another/folder/view_name", [** optional arguments**]);
Views locations
There are two main locations where to find views:
- Generic views: within
root/src/views; - Custom paths: additional locations declared by the user;
Additional paths can be added from the views configuration file (root/config/views.php) or by using the methods provided by the View class.
Below is a basic example of a custom view locations declaration:
use \App\System\MVC\View;
//Add an additional location to find views. The namespace is useful if multiple locations have views with the same name.
View::addSourcePath("/absolute/path/to/views/directory", "OptionalNamespace");
//If you used a namespace you must use it to use the view
view("OptionalNamespace::view_name", [** optional arguments**]);
//Else you can omit it
view("view_name", [** optional arguments**]);
//Add an additional location to find modules views. The usage is the same as above but in this case namespace is mandatory and MUST match the module name.
View::addModuleSourcePath("/absolute/path/to/views/directory", "MandatoryNamespace");
//To use module views you MUST use module_view helper or View::module method
module_view("MandatoryNamespace::view_name", [** optional arguments**]);
View::module("MandatoryNamespace::view_name", [** optional arguments**]);
Before proceeding, it's crucial to understand how views are searched and their priority levels.
Module Views
Module views can be found in two main paths, and are searched in the following order of priority:
- Project override:
src/views/{ModuleNamespace}, inside the project itself. This lets a project override a specific module view without touching the module. It's also wherecopyResources()publishes a module's views when itsviewsresource is copied (see the Modules section); - Module's own views: the path registered by the module itself via
View::addModuleSourcePath(), usually the module's ownviewsfolder.
If the view does not exist in any of the paths, an exception will be thrown.
For further details, see the modules documentation.
Creating and using a view
Views can be created wherever you like, but it's recommended to use the provided paths so you can use the features we've just discussed. Create the "my_first_view.php" file within root/src/views and write the following content:
Now declare the route "test-view" and write the following code:
router()->get("/test-view", function() {
return view("my_first_view", [
"message" => "Hello World!"
]);
});
When you visit monti.loc/test-view, you should see the message "Hello World!".
Check if a view exists
Render a view
echo view("view_name");
//or
echo view("view_name")->render();
//Render a view without printing the contents
$view_content = view("view_name")->render();
Extend a view
Similar to classes, a view can be extended for reuse by changing only the necessary elements. Let's take the "html" view (root/src/views/html.php) as an example, as it represents a complete example of view usage (also useful for the explanations that follow), and modify the previous view as follows:
<?php $this->parent(view("html")) ?>
<?php $this->start_section("body") ?>
<h1><?=$message?></h1>
<?php $this->stop_section() ?>
Going in order, the first statement declares that you want to use the "html" view as the parent view. Before explaining how start_section works, let's analyze the "html" view. Note the following code:
The section method takes an identifying name as its argument, each section can be considered a placeholder that identifies the position the content will occupy within the parent view. The start_section method takes the section's identifying name as its first argument, indicating the beginning of the content, the stop_section method indicates the end of the content. In short: placeholders are declared in the parent view, their content is declared in the child view.
Visiting monti.loc/test-view again, and inspecting the code, we can see how "test-view" is included inside the body of the "html" view.
The Document class, styles and scripts
It's possible to add style and scripts to an HTML page using sections and extending a parent view, but in most cases we recommend using the start_style/stop_style, start_script/stop_script, and Document class methods. Although very basic, the Document class performs a sort of DOM virtualization, offering some useful methods. Read the Document documentation to see how it works and how to use it, or the Monti API for the full method reference.
Style sections
Like sections (start_section/stop_section), style sections are delimited by start_style and stop_style. Style and link tags can be declared within them. Once declared, they are processed and held aside until they are printed. Multiple style sections can be opened, both in parents and children. Finally, by calling the \Document::get()->printStyles() method, styles will be processed from parent to children (in the case of a view extension) and printed in the document. In other words, from the outside (parent) to the inside (children).
By calling stop_style you can pass an array of options. Currently only one option is available:
- id: assigns an id to the section. This can be useful when the same view is used multiple times on the same page, thus preventing the same styles from being printed multiple times;
Opt-* attributes
Like the stop_style options, it is possible to assign special attributes to style tags. Currently, only one opt-* attribute is available:
- opt-group="{{group-name}}": group style tags into a single style tag. Multiple views containing different style sections may be used on the same page. Using the opt-group attribute, you can prevent multiple style tags from being printed and combine them into a single style tag;
Styles will be chained in declaration order, from the innermost child view to the parent if the view is extended.
Script sections
Script sections work the same way as the style sections explained above.
Printing style and script sections
Once you have declared the style sections and script sections, you need to call the \Document::get()->printStyles() and \Document::get()->printScripts() methods respectively to print them in the document.