The installation of this library can be done with Composer:
composer require aplus/mvc
App
The App class is the MVC "kernel". It is through it that an application
receives and responds to CLI and HTTP requests. It contains the various services
that can be called from anywhere, with multiple instances with various predefined
configurations.
Using it directly or extending it is optional. In it, services communicate
internally and are used in Controllers and Models.
It is designed to integrate several Aplus Framework libraries and, in a simple
way, provide a powerful application.
Initialization:
use Framework\MVC\App;
$app = new App();
The App class first param can receive a
Config
instance, config options as array or a config directory path as string.
For example:
$app = new App(__DIR__ . '/configs');
After initialization, it is also possible set configurations.
Let's see an example using the view and database services.
App handles Command-Line Interface with the Console Service.
Let's create the console.php file:
use Framework\MVC\App;
$app = new App();
$app::config()->set('console', [
'directories' => [
__DIR__ . '/commands',
]
]);
$app->runCli(); // void
Now, let's add a command in the commands/Meet.php file:
use Framework\CLI\CLI;
use Framework\CLI\Command;
class Meet extends Command
{
public function run() : void
{
$name = CLI::prompt('What is your name?', 'Tadandan');
CLI::write("Nice to meet you, $name. I'm Aplus.");
}
}
Go to the terminal and run:
php console.php
The console will output meet as an available command.
To execute it, run:
php console.php meet
That's it.
Services
Services are static methods in the App class. With them it is possible to make
quick calls with predefined configurations for different object instances, with
automated dependency injection.
Set a Language Service instance name. If not set, the Language instance will
not be passed.
developmentview
Set the file path to a view when in the development environment.
productionview
Set the file path to a view when in the production environment.
initialize
Set if it is to initialize by setting the class as exception handler. The
default value is true.
searchengine
Set the search engine used to create search links on exception error messages.
Valid values are: ask, baidu, bing, duckduckgo, google,
yahoo and yandex. The default is google.
handleerrors
If initialize is true, this option defines whether to set the class as an
error handler. The default value is true.
It can take an array of directives to initialize an instance of
Framework\HTTP\CSP and pass it as the Content-Security-Policy-Report-Only
of the response.
jsonflags
Flags for json_encode. The default is set to none.
requestinstance
Set the Request Service instance name. The default is default.
Sets a callback to be executed when the Router starts up. The callback receives
the Router instance in the first parameter.
By default no callback is set.
files
Sets an array with the path of files that will be inserted to serve the routes.
The default is to set none.
placeholders
A custom placeholder array. Where the key is the placeholder and the value is
the pattern. The default is to set none.
autooptions
If set to true it enables the feature to automatically respond to OPTIONS
requests. The default is no set.
automethods
If set to true it enables the feature to respond with the status 405 Method
Not Allowed and the Allow header containing valid methods. The default is no set.
responseinstance
Set the Response Service instance name. The default value is default.
Optional. Sets an array containing the class key with the Fully Qualified
Class Name of a child class of Framework\Session\SaveHandler. And also the
config key with the configurations passed to the SaveHandler.
If the class is an instance of Framework\Session\SaveHandlers\DatabaseHandler
it is possible to set the instance of a Database Service through the key database_instance.
options
Sets an array with the options to be passed in the construction of the Session
class.
autostart
Set to true to automatically start the session when the service is called.
The default is not to start.
loggerinstance
Set the Logger Service instance name. If not set, the Logger instance will
not be set in the save handler.
Models represent tables in databases. They have basic CRUD (create, read, update
and delete) methods, with validation, localization and performance
optimization with caching and separation of read and write data.
To create a model that represents a table, just create a class that extends the
Framework\MVC\Model class.
use Framework\MVC\Model;
class Users extends Model
{
}
The example above is very simple, but with it it would be possible to read data
in the Users table.
Table Name
The table name can be set in the $table property.
protected string $table;
If the name is not set the first time the getTable method is called, the
table name will automatically be set to the class name. For example, if the
class is called App\Models\PostsModel, the table name will be Posts.
Database Connections
Each model allows two database connections, one for reading and one for writing.
The connections are obtained through the Framework\MVC\App::database method
and the name of the instances must be defined in the model.
To set the name of the read connection instance, use the $connectionRead
property.
protected string $connectionRead = 'default';
The name of the connection can be obtained through the getConnectionRead
method and the instance of Framework\Database\Database can be obtained
through the getDatabaseToRead method.
The name of the write connection, by default, is also default. But it can
be modified through the $connectionWrite property.
protected string $connectionWrite = 'default';
The name of the write connection can be taken by the getConnectionWrite
method and the instance by getDatabaseToWrite.
Primary Key
To work with a model, it is necessary that its database table has an
auto-incrementing Primary Key, because it is through it that data is found by
the read method, and rows are also updated and deleted.
By default, the name of the primary key is id, as in the example below:
protected string $primaryKey = 'id';
It can be obtained with the getPrimaryKey method.
The primary key field is protected by default, preventing it from being changed
in write methods such as create, update and replace:
protected bool $protectPrimaryKey = true;
You can check if the primary key is being protected by the
isProtectPrimaryKey method.
If it is protected, but allowed in Allowed Fields, an exception will be
thrown saying that the primary key field is protected and cannot be changed by
writing methods.
Allowed Fields
To manipulate a table making changes it is required that the fields allowed for
writing are set, otherwise a LogicException will be thrown.
Using the $allowedFields property, you can set an array with the names of
the allowed fields:
This list can be obtained with the getAllowedFields method.
Note that the data is filtered on write operations, removing all disallowed
fields.
Return Type
When reading data, by default the rows are converted into stdClass objects,
making it easier to work with object orientation.
But, the $returnType property can also be set as array (making the
returned rows an associative array) or as a class-string of a child class of
Framework\MVC\Entity.
protected string $returnType = stdClass::class;
The return type can be obtained with the getReturnType method.
Results are automatically converted to the return type in the read,
list and paginate methods.
Automatic Timestamps
With models it is possible to save the creation and update dates of rows
automatically.
To do this, just set the $autoTimestamps property to true:
protected bool $autoTimestamps = false;
To find out if automatic timestamps are enabled, you can use the
isAutoTimestamps method.
By default, the name of the field with the row creation timestamp is
createdAt and the field with the update timestamp is called updatedAt.
Both fields can be changed via the $fieldCreated and $fieldUpdated
properties:
And, the format can be obtained through the getTimestampFormat method.
The timestamps are generated using the timezone of the write connection and,
if it is not set, it uses UTC.
A timestamp formatted in $timestampFormat can be obtained with the
getTimestamp method.
When the fields of $fieldCreated or $fieldUpdated are set to
$allowedFields they will not be removed by filtering, and you can set
custom values.
Validation
When one of the create, update or replace methods is called for the
first time, the $validation property will receive an instance of
Framework\Validation\Validation for exclusive use in the model, which can be
obtained by the getValidation method.
To make changes it is required that the validation rules are set, otherwise a
RuntimeException will be thrown saying that the rules were not set.
You can set the rules in the $validationRules property:
After calling the paginate method, the $pager property will have an
instance of the Framework\Pagination\Pager class, which can be obtained by
the getPager method.
So you can render the pagination wherever you need it, like in the HTTP Link
header or as HTML in views:
echo $model->getPager()->render('bootstrap');
In the $pagerView property you can define the default Pager view:
protected string $pagerView = 'bootstrap';
So this view will always render by default:
echo $model->getPager()->render();
Also, it is possible to set the Pager URL in the $pagerUrl property, which
is unnecessary in HTTP requests, but required in the command line.
Cache
The model has a cache system that works with individual results. For example,
once the $cacheActive property is set to true, when obtaining a row
via the read method, the result will be stored in the cache and will be
available directly from it for the duration of the Time To Live, defined in the
$cacheTtl property.
When an item is updated via the update method, the cached item will also be
updated.
When an item is deleted from the database, it is also deleted from the cache.
With the active caching system it reduces the load on the database server, as
the rows are obtained from files or directly from memory.
Below is the example with the cache inactive. To activate it, just set the value
to true.
protected bool $cacheActive = false;
Whenever you want to know if the cache is active, you can use the
isCacheActive method.
And the name of the service instance obtained through the method
Framework\MVC\App::cache can be set as in the property below:
protected string $cacheInstance = 'default';
Whenever it is necessary to get the name of the cache instance, you can use the
getCacheInstance method and to get the object instance of the
Framework\Cache\Cache class, you can use the getCache method.
The default Time To Live value for each item is 60 seconds, as shown below:
protected int $cacheTtl = 60;
This value can be obtained through the getCacheTtl method.
Language
Some features, such as validation, on labels and error messages, or pagination
need an instance of Framework\Language\Language to locate the displayed
texts.
The name of the instance defined in the $languageInstance property is
obtained through the service available in the Framework\MVC\App::language
method, and can be obtained through the getLanguage method.
The default instance is default, as shown below:
protected string $languageInstance = 'default';
CRUD
The model has methods to work with basic CRUD operations, which are:
The create method inserts a new row and returns the LAST_INSERT_ID() on
success or false if validation fails:
$data = [
'name' => 'John Doe',
'email' => '[email protected]',
];
$id = $model->create($data); // Insert ID or false
If it returns false, it is possible to get the errors through the
getErrors method:
if ($id === false) {
$errors = $model->getErrors();
}
Read
The read method reads a row based on the Primary Key and returns the row
with the type set in the $returnType property or null if the row is not
found.
$id = 1;
$row = $model->read($id);
It is also possible to read all rows, with limit and offset, by returning an
array with items in the $returnType.
Entities represent rows in a database table. They can be used as a Return Type
in models.
Let's see the entity User below:
use Framework\Date\Date;
use Framework\HTTP\URL;
use Framework\MVC\Entity;
class User extends Entity
{
protected int $id;
protected string $name;
protected string $email;
protected string $passwordHash;
protected URL $url;
protected Date $createdAt;
protected Date $updatedAt;
}
The array keys will be set as the property name with their respective values in
the populate method.
If a setter method exists for the property, it will be called. For example, if
there is a setId method, it will be called to set the id property. If
the setId method does not exist, it will try to set the property, if it
exists, otherwise it will throw an exception saying that the property is not
defined. If set, it will attempt to set the value to the property's type,
casting type with the Type Hints methods.
Init
The init method is used to initialize settings, set custom properties, etc.
Called in the constructor just after the properties are populated.
Properties can be called directly. But first, it is always checked if there is
a getter for it and if there is, it will be used:
$id = $user->id; // 1
$id = $user->getId(); // 1
Magic Setters
Properties can be set directly. But before that, it is always checked if there
is a setter for it and if there is, the value will be set through it:
$user->id = 3;
$user->setId(3);
Type Hints
It is common to need to convert types when setting property. For example,
setting a URL string to be converted as an object of the Framework\HTTP\URL
class.
Before a property is set, the Entity class checks the property's type and checks
the value's type. Then, try to convert the value to the property's type through
3 methods.
Each method must return the value in the converted type or null, indicating that
the conversion was not performed.
Type Hint Custom
The typeHintCustom method must be overridden to make custom type changes.
Type Hint Native
The typeHintNative method converts to native PHP types, which are:
array, bool, float, int, string and stdClass.
Type Hint Aplus
The typeHintAplus method converts to Aplus Framework class types, which are:
Framework\Date\Date and Framework\HTTP\URL.
To Model
Through the toModel method, the object is transformed into an associative
array ready to be written to the database.
Conversion to array can be done directly, as below:
$data = $user->toModel(); // Associative array
Or passed directly to one of a model's methods.
Let's see how to create a row using the variable $user, which is an entity:
$id = $model->create($user); // Insert ID or false
Working with Timezones
Let's see below how to work with timezones when exporting data to models.
use Framework\Date\Date;
use Framework\MVC\Entity;
class User extends Entity
{
public string $_timezone = '+00:00'; // Default value
protected Date $createdAt;
}
We set PHP's default timezone to America/SaoPaulo (-03:00):
date_default_timezone_set('America/Sao_Paulo');
The User object is created:
$user = new User([
'createdAt' => '2024-08-02 10:30:00',
]);
We advise you to leave the Entity and database timezones with the default value
of +00:00 (UTC). But if you really need to modify it don't forget to do it
when you connect the database
and put the same timezone in the Entity.
JSON Encoding
JSON Vars
When working with APIs, it may be necessary to convert an Entity to a JSON
object.
To set which properties will be JSON-encoded just list them in the property
$_jsonVars:
use Framework\Date\Date;
use Framework\HTTP\URL;
use Framework\MVC\Entity;
class User extends Entity
{
public array $_jsonVars = [
'id',
'name',
'url',
'createdAt',
'updatedAt',
'bio',
];
protected int $id;
protected string $name;
protected ?string $bio = null;
protected string $email;
protected string $passwordHash;
protected URL $url;
protected Date $createdAt;
protected Date $updatedAt;
}
Note that the url and createdAt property objects have been json-serialized.
And, only properties with defined values will appear in the result. Note that
bio has the default value of null and appears in the result. On the other
hand, the updatedAt property does not have a defined value and therefore does
not appear in the result.
JSON Flags
Through the $_jsonFlags property, the flags to encode and decode JSON
internally in the Entity class are set.
The example below shows the flags with the default value:
class User extends Entity
{
public int $_jsonFlags = JSON_UNESCAPED_SLASHES
| JSON_UNESCAPED_UNICODE
| JSON_PRESERVE_ZERO_FRACTION
| JSON_THROW_ON_ERROR;
}
The rule has two parameters: $tableColumn and $connection.
$tableColumn is the table name and, optionally, the column name separated by
a dot. If the column is not defined, the field name will be used as the column name.
$connection is the name of the database connection. The default is default.
The rule has four parameters: $tableColumn, $ignoreColumn,
$ignoreValue and $connection.
$tableColumn is the table name and, optionally, the column name separated by
a dot. If the column is not defined, the field name will be used as the column name.
$ignoreColumn is the name of the column to ignore if the value is already
registered. Usually when updating data.
$ignoreValue is the value to be ignored in the $ignoreColumn.
$connection is the name of the database connection. The default is default.
Views
To obtain a View instance, the class can be instantiated individually, as shown
in the example below:
use Framework\MVC\View;
$baseDir = __DIR__ . '/views';
$extension = '.php';
$view = new View($baseDir, $extension);
Or getting an instance of the view service in the App class:
use Framework\MVC\App;
$view = App::view();
With the View instantiated, we can render files.
The file below will be used on the home page and is located at
views/home/index.php:
<h1><?= $title ?></h1>
<p><?= $description ?></p>
Returning to the main file, we pass the data to the file to be rendered:
In the views/_layouts/default.php file we can render the two blocks:
<body>
<?= $view->renderBlock('contents') // string or null ?>
<?= $view->renderBlock('scripts') // string or null ?>
</body>
And the output will be like this:
<body>
<h1>Welcome!</h1>
<p>Welcome to Aplus MVC.</p>
<script>
console.log('Hello!');
</script>
</body>
Controllers
The abstract class Framework\MVC\Controller extends the class
Framework\Routing\RouteActions, inheriting the characteristics necessary
for your methods to be used as route actions.
Below we see an example with the Home class and the index action method
returning a string that will be appended to the HTTP Response body:
use Framework\MVC\Controller;
class Home extends Controller
{
public function index() : string
{
return 'Home page.'
}
}
Render Views
Instead of building all the page content inside the index method, you can
use the render method, with the name of the file that will be rendered,
building the HTML page as a view.
In this case, we render the home/index view:
use Framework\MVC\Controller;
class Home extends Controller
{
public function index() : string
{
return $this->render('home/index');
}
}
Validate Data
When necessary, you can validate data using the validate method.
In it, it is possible to put the data that will be validated, the rules, and,
optionally, the labels, the messages and the name of the validation service
instance, which by default is default.
In the example below we highlight the create method, which can be called by
the HTTP POST method to create a contact message.
Note that the rules are set and then the POST data is validated, returning an
array with the errors and showing them on the screen in a list or an empty array,
showing that no validation errors occurred and the message that was created
successfully:
use Framework\MVC\Controller;
class Contact extends Controller
{
public function index() : string
{
return $this->render('contact/index');
}
public function create() : void
{
$rules = [
'name' => 'minLength:5|maxLength:32',
'email' => 'email',
'message' => 'minLength:10|maxLength:1000',
];
$errors = $this->validate($this->request->getPost(), $rules);
if ($errors) {
echo '<h2>Validation Errors</h2>';
echo '<ul>';
foreach($errors as $error) {
echo '<li>' . $error . '</li>';
}
echo '</ul>';
return;
}
echo '<h2>Contact Successful Created</h2>';
}
}
HTTP Request and Response
The Controller has instances of the two HTTP messages, the Request and the
Response, accessible through properties that can be called directly.
Let's see below how to use Request to get the current URL as a string and put it
in the Response body:
use Framework\HTTP\Response;
use Framework\MVC\Controller;
class Home extends Controller
{
public function index() : Response
{
$url = (string) $this->request->getUrl();
return $this->response->setBody(
'Current URL is: ' . $url
);
}
}
The example above is simple, but $request and $response are powerful, having
numerous useful methods for working on HTTP interactions.
Model Instances
Each controller can have model instances in properties, which will be automatically
instantiated in the class constructor.
The properties must be child classes of the Framework\MVC\Model class.
Let's see below that $users receives the type name of the
App\Models\UsersModel class and in the show method the direct call to
the $users property is used, which has the instance of
App\Models\UsersModel:
use App\Models\UsersModel;
use Framework\MVC\Controller;
class Users extends Controller
{
protected UsersModel $users;
public function show(int $id) : string
{
$user = $this->users->read($id);
return $this->render('users/show', [
'user' => $user,
]);
}
}
JSON Responses
As with the Framework\Routing\RouteActions class, the controller action methods
can return an array, stdClass, or JsonSerializable instance so that the Response
is automatically set with the JSON Content-Type and the message body as well.
In the example below, we see how to get the users of a page, with an array
returned from the model's paginate method, and then returned to be
JSON-encoded and added to the Response body:
use App\Models\UsersModel;
use Framework\MVC\Controller;
class Users extends Controller
{
protected UsersModel $users;
public function index() : array
{
$page = $this->request->getGet('page')
$users = $this->users->paginate($page);
return $users;
}
}
Before and After Actions
Every controller has two methods inherited from Framework\Routing\RouteActions
that can be used to prepare configurations, filter input data, and also to
finalize configurations and filter output data.
They are beforeAction and afterAction.
Let's look at a simple example to validate a user's access to a dashboard's pages.
We create the AdminController class and put a check in it to see if the
user_id is set in the session. If not, the page will be redirected to the
location /login. Otherwise, access to the action method is released and
the user can access the admin area:
use Framework\MVC\App;
use Framework\MVC\Controller;
abstract class AdminController extends Controller
{
protected function beforeAction(string $method, array $arguments) : mixed
{
if (!App::session()->has('user_id')) {
return $this->response->redirect('/login');
}
return null;
}
}
Below, the Dashboard methods will only be executed if beforeAction returns
null in the parent class, AdminController:
final class Dashboard extends AdminController
{
public function index() : string
{
return 'You are in Admin Area!';
}
}
Conclusion
Aplus MVC Library is an easy-to-use tool for, beginners and experienced, PHP developers. It is perfect to create simple, fast and powerful MVC applications. The more you use it, the more you will learn.
Did you find something wrong? Be sure to let us know about it with an
issue. Thank you!