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.
It is designed to integrate several Aplus Framework libraries and, in a simple
way, provide a powerful application.
After initialization, it is also possible set configurations.
Then the service config instances will be available.
Running
App is designed to run HTTP and run CLI requests, sharing the same services.
Run HTTP
App handles the internet Hypertext Transfer Protocol in a very easy-to-use way.
Let's see an example creating a little application:
We will need to autoload classes, so we will set default configs for the
Autoloader Service.
This app will respond to two origins. One is the web front end. Another is the REST API.
The default Router Service will load one file for each origin.
This is the public/index.php file:
use Framework\MVC\App;
(new App([
'autoloader' => [
'default' => [
'namespaces' => [
'Api' => __DIR__ . '/../api',
],
],
],
'router' => [
'default' => [
'files' => [
__DIR__ . '/../routes/front.php',
__DIR__ . '/../routes/api.php',
],
],
],
]))->runHttp(); // void
And now, let's create the router files:
The routes/front.php file is for the front end. The origin will be
https://domain.tld. Change if you want:
use Framework\MVC\App;
use Framework\Routing\RouteCollection;
App::router()->serve('https://domain.tld', function (RouteCollection $routes) {
$routes->get('/', fn () => '<h1>Homepage</h1>');
}); // static
The routes/api.php is for the REST API. The origin will be
https://api.domain.tld. Change if you need:
use Framework\MVC\App;
use Framework\Routing\RouteCollection;
App::router()->serve('https://api.domain.tld', function (RouteCollection $routes) {
$routes->get('/', fn () => App::router()->getMatchedCollection());
$routes->post('/users', 'Api\UsersController::create');
$routes->get('/users/{int}', 'Api\UsersController::show/0', 'users.show');
}, 'api'); // static
This is the api/UsersController.php example:
namespace Api;
use Framework\HTTP\Response;
use Framework\HTTP\ResponseHeader;
use Framework\HTTP\Status;
use Framework\MVC\App;
use Framework\MVC\Controller;
class UsersController extends Controller
{
public function create() : Response
{
$data = $this->request->getPost();
$errors = $this->validate($data, [
'name' => 'minLength:5|maxLength:64',
'email' => 'email',
]);
if ($errors) {
return $this->response
->setStatus(Status::BAD_REQUEST)
->setJson([
'errors' => $errors,
]);
}
// TODO: Create the UsersModel to insert the new user
// ...
$user = [
'id' => rand(1, 1000000),
'name' => $data['name'],
'email' => $data['email'],
];
return $this->response
->setStatus(Status::CREATED)
->setHeader(
ResponseHeader::LOCATION,
App::router()->getNamedRoute('api.users.show')
->getUrl(pathArgs: [$user['id']])
)->setJson($user);
}
}
After that, the application will have the following files:
-
public/index.php
-
routes/front.php
-
routes/api.php
-
api/UsersController.php
Put you server to run and access the URLs https://domain.tld and
https://api.domain.tld.
You can make a POST request with curl to https://api.domain.tld/users:
curl -i -X POST https://api.domain.tld/users \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "name=John&[email protected]"
That's it. The basic HTTP application structure is created and working.
You can improve it with Models, Views and Controllers.
Run CLI
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.
App services can be extended. See Extending.
Built-in services:
Anti-CSRF Service
Gets an instance of
Framework\HTTP\AntiCSRF.
App::antiCsrf()
Anti-CSRF Config Options
[
'antiCsrf' => [
'default' => [
'enabled' => true,
'token_name' => 'csrf_token',
'session_instance' => 'default',
'request_instance' => 'default',
],
],
]
enabled
Set true
to enable and false
to disable. By default it is enabled.
tokenname
Sets the token name. The default is csrf_token
.
sessioninstance
Set the Session Service instance name. The default is default
.
requestinstance
Set the Request Service instance name. The default is default
.
Autoloader Service
Gets an instance of
Framework\Autoload\Autoloader.
App::autoloader()
Autoloader Config Options
[
'autoloader' => [
'default' => [
'register' => true,
'extensions' => '.php',
'namespaces' => null,
'classes' => null,
],
],
]
register
Set true
to register as an autoload function or false
to disable. The
default is to leave it enabled.
extensions
A comma-separated list of extensions. The default is .php
.
namespaces
Sets the mapping from namespaces to directories. By default it is not set.
classes
Sets the mapping of classes to files. By default it is not set.
Cache Service
Gets an instance of
Framework\Cache\Cache.
App::cache()
Cache Config Options
[
'cache' => [
'default' => [
'class' => ???, // Must be set
'configs' => [],
'prefix' => null,
'serializer' => Framework\Cache\Serializer::PHP,
'logger_instance' => 'default',
],
],
]
class
The Fully Qualified Class Name of a class that extends Framework\Cache\Cache
.
There is no default value, it must be set.
configs
The configurations passed to the class. By default it is an empty array.
prefix
A prefix for the name of cache items. By default nothing is set.
serializer
Sets the serializer with a value from the enum Framework\Cache\Serializer,
which can be a case of the enum or a string.
The default value is Framework\Cache\Serializer::PHP
.
loggerinstance
Set the Logger Service instance name. If not set, the Logger instance will
not be set in the Cache class.
Console Service
Gets an instance of
Framework\CLI\Console.
App::console()
Console Config Options
[
'console' => [
'default' => [
'directories' => null,
'find_in_namespaces' => false,
'language_instance' => 'default',
'locator_instance' => 'default',
],
],
]
directories
Sets an array of directories where commands will be looked for. By default there
is no directory.
findinnamespaces
Set true
to find commands in all Commands subdirectories of all namespaces.
The default is not to find in namespaces.
Set a Language Service instance name. If not set, the Language instance will
not be set in the Console class.
locatorinstance
Set the Locator Service instance name. By default it is default
.
Database Service
Gets an instance of
Framework\Database\Database.
App::database()
Database Config Options
[
'database' => [
'default' => [
'config' => ???, // Must be set
'logger_instance' => 'default',
],
],
]
config
Set an array of configurations. Usually just the username
, the password
and the schema
.
The complete list of configurations can be seen
here.
loggerinstance
Set the Logger Service instance name. If not set, the Logger instance will
not be set in the Database class.
Debugger Service
Gets an instance of
Framework\Debug\Debugger.
App::debugger()
Debugger Config Options
[
'debugger' => [
'default' => [
'debugbar_view' => null,
'options' => null,
],
],
]
debugbarview
Sets the path of a file to be used instead of the debugbar view. The default is
to use the original.
options
Sets an array of options for the Debugger. The default is to set nothing.
Exception Handler Service
Gets an instance of
Framework\Debug\ExceptionHandler.
App::exceptionHandler()
Exception Handler Config Options
[
'exceptionHandler' => [
'default' => [
'environment' => Framework\Debug\ExceptionHandler::PRODUCTION,
'logger_instance' => 'default',
'language_instance' => 'default',
'development_view' => null,
'production_view' => null,
'initialize' => true,
'handle_errors' => true,
],
],
]
environment
Set the environment, default is production. Use the ExceptionHandler::DEVELOPMENT
or ExceptionHandler::PRODUCTION
constants.
loggerinstance
Set the Logger Service instance name. If not set, the Logger instance will
not be set in the ExceptionHandler class.
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
.
handleerrors
If initialize is true
, this option defines whether to set the class as an
error handler. The default value is true
.
Language Service
Gets an instance of
Framework\Language\Language.
App::language()
Language Config Options
[
'language' => [
'default' => [
'default' => 'en',
'current' => 'en',
'supported' => null,
'negotiate' => false,
'request_instance' => 'default',
'fallback_level' => Framework\Language\FallbackLevel::none,
'directories' => [],
'find_in_namespaces' => false,
'autoloader_instance' => 'default',
],
],
]
default
Sets the default language code. The default is en
.
current
Sets the current language code. The default is en
.
supported
Set an array with supported languages. The default is to set none.
negotiate
Set true
to negotiate the locale on the command line or HTTP request.
requestinstance
Set the Request Service instance name to negotiate the current locale. The
default is default
.
fallbacklevel
Sets the Fallback Level to a Framework\Language\FallbackLevel enum case or an
integer. The default is to set none.
directories
Sets directories that contain subdirectories with the locale and language files.
The default is to set none.
findinnamespaces
If set to true
it will cause subdirectories called Language to be searched
in all namespaces and added to Language directories.
autoloaderinstance
Sets the Autoloader Service instance name of the autoloader used to find in
namespaces.
Locator Service
Gets an instance of
Framework\Autoload\Locator.
App::locator()
Locator Config Options
[
'locator' => [
'default' => [
'autoloader_instance' => 'default',
],
],
]
Logger Service
Gets an instance of
Framework\Log\Logger.
App::logger()
Logger Config Options
[
'logger' => [
'default' => [
'class' => Framework\Log\Logger\MultiFileLogger::class,
'destination' => ???, // Must be set
'level' => Framework\Log\LogLevel::DEBUG,
'config' => [],
],
],
]
class
A Fully Qualified Class Name of a child class of Framework\Log\Logger.
The default is Framework\Log\Logger\MultiFileLogger
.
destination
Set the destination where the logs will be stored or sent. It must be set
according to the class used.
level
Sets the level of logs with a case of Framework\Log\LogLevel or an integer. If
none is set, the DEBUG
level will be used.
config
Sets an array with extra configurations for the class. The default is to pass an
empty array.
Mailer Service
Gets an instance of
Framework\Email\Mailer.
App::mailer()
Mailer Config Options
[
'mailer' => [
'default' => [
'class' => Framework\Email\Mailers\SMTPMailer::class,
'config' => ???, // Must be set
],
],
]
class
Sets the Fully Qualified Class Name of a child class of Framework\Email\Mailer.
The default is Framework\Email\Mailers\SMTPMailer
.
config
Set an array with Mailer settings. Normally you just set the username
, the
password
, the host
and the port
.
The complete list of configurations can be seen
here.
Migrator Service
Gets an instance of
Framework\Database\Extra\Migrator.
App::migrator()
Migrator Config Options
[
'migrator' => [
'default' => [
'database_instance' => 'default',
'directories' => ???, // Must be set
'table' => 'Migrations',
],
],
]
databaseinstance
Set the Database Service instance name. The default is default
.
directories
Sets an array of directories that contain Migrations files. It must be set.
table
The name of the migrations table. The default name is Migrations
.
Request Service
Gets an instance of
Framework\HTTP\Request.
App::request()
Request Config Options
[
'request' => [
'default' => [
'server_vars' => [],
'allowed_hosts' => [],
'force_https' => false,
],
],
]
servervars
An array of values to be set in the $_SERVER superglobal on the command line.
allowedhosts
Sets an array of allowed hosts. The default is an empty array, so any host is allowed.
forcehttps
Set true
to automatically redirect to the HTTPS version of the current URL.
By default it is not set.
Response Service
Gets an instance of
Framework\HTTP\Response.
App::response()
Response Config Options
[
'response' => [
'default' => [
'headers' => [],
'auto_etag' => false,
'auto_language' => false,
'language_instance' => 'default',
'cache' => null,
'csp' => [],
'csp_report_only' => [],
'request_instance' => 'default',
],
],
]
autoetag
true
allow to enable ETag auto-negotiation on all responses. It can also be
an array with the keys active
and hash_algo
.
autolanguage
Set true
to set the Content-Language header to the current locale. The
default is no set.
cache
Set false
to set Cache-Control to no-cache
or an array with key seconds
to set cache seconds and optionally public
to true or false to private
.
The default is not to set these settings.
csp
If it is empty, it does nothing.
It can take an array of directives to initialize an instance of
Framework\HTTP\CSP
and pass it as the Content-Security-Policy of the
response.
cspreportonly
If it is empty, it does nothing.
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.
requestinstance
Set the Request Service instance name. The default is default
.
Router Service
Gets an instance of
Framework\Routing\Router.
App::router()
Router Config Options
[
'router' => [
'default' => [
'files' => [],
'placeholders' => [],
'auto_options' => null,
'auto_methods' => null,
'response_instance' => 'default',
'language_instance' => 'default',
],
],
]
callback
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
.
Set a Language Service instance name. If not set, the Language instance will
not be passed.
Session Service
Gets an instance of
Framework\Session\Session.
App::session()
Session Config Options
[
'session' => [
'default' => [
'save_handler' => [
'class' => null,
'config' => [],
],
'options' => [],
'auto_start' => null,
'logger_instance' => '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.
Validation Service
Gets an instance of
Framework\Validation\Validation.
App::validation()
Validation Config Options
[
'validation' => [
'default' => [
'validators' => [
Framework\MVC\Validator::class,
Framework\Validation\FilesValidator::class,
],
'language_instance' => 'default',
],
],
]
validators
Sets an array of Validators. The default is an array with the Validator from the
mvc package and the FilesValidator from the Validation package.
Set the Language Service instance name. The default is not to set an
instance of Language.
View Service
Gets an instance of
Framework\MVC\View.
App::view()
View Config Options
[
'view' => [
'default' => [
'base_dir' => ???, // Must be set
'extension' => '.php',
'layout_prefix' => null,
'include_prefix' => null,
'show_debug_comments' => true,
],
],
]
basedir
Sets the base directory from which views will be loaded. The default is to not
set any directories.
extension
Sets the extension of view files. The default is .php
.
layoutprefix
Sets the layout prefix. The default is to set none.
includeprefix
Set the includes prefix. The default is to set none.
Extending
Built-in services are designed to be extended.
Let's look at an example extending the HTTP Request class and adding two custom
methods, isGet
and isPost
:
namespace Lupalupa\HTTP;
class Request extends \Framework\HTTP\Request
{
public function isGet() : bool
{
return $this->hasMethod('get');
}
public function isPost() : bool
{
return $this->hasMethod('post');
}
}
Now, let's extend the App class:
The App::request
method return type can be replaced by a child class thanks
to Covariance.
The example below adds a new method, App::other
, which also uses
Late Static Bindings.
use Lupalupa\HTTP\Request;
use Lupalupa\Other;
class App extends \Framework\MVC\App
{
public static function request(string $instance = 'default') : Request
{
$service = static::getService('request', $instance);
if ($service) {
return $service;
}
$config = static::config()->get('request', $instance);
return static::setService(
'request',
new Request($config['allowed_hosts'] ?? null),
$instance
);
}
public static function other(string $instance = 'default') : Other
{
$service = static::getService('other', $instance);
if ($service) {
return $service;
}
$config = static::config()->get('other', $instance);
$service = new Other(
static::request($config['request_instance'] ?? 'default')
);
if (isset($config['foo'])) {
$service->setFoo($config['foo']);
}
return static::setService('other', $service, $instance);
}
}
Finally, you will be able to use the custom instance of Request
and Other
anywhere in your application:
App::request()->isGet();
App::request()->isPost();
App::other()->doSomething();
App::other('other_instance')->doSomething();
Tip: Use a smart IDE. Aplus loves it.
Be happy.
Models
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:
protected array $allowedFields = [
'name',
'email',
];
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:
protected string $fieldCreated = 'createdAt';
protected string $fieldUpdated = 'updatedAt';
To get the name of the automatic timestamp fields you can use the
getFieldCreated
and getFieldUpdated
methods.
The timestamp format can also be customized. The default is like the example
below:
protected string $timestampFormat = 'Y-m-d H:i:s';
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:
protected array $validationRules = [
'name' => 'minLength:5|maxLength:32',
'email' => 'email',
];
Or returning in the getValidationRules
method.
Validators can also be customized. By default, the ones used are
Framework\MVC\Validator and Framework\Validation\FilesValidator:
protected array $validationValidators = [
Validator::class,
FilesValidator::class,
];
The list of validators can be obtained using the getValidationValidators
method.
The labels with the name of the fields in the error messages can also be
customized, being set in the $validationLabels
property:
protected array $validationLabels;
Or through the getValidationLabels
method, as in the example below, setting
the labels in the current language:
protected function getValidationLabels() : array
{
return $this->validationLabels ??= [
'name' => $this->getLanguage()->render('users', 'name'),
'email' => $this->getLanguage()->render('users', 'email'),
];
}
The same goes for setting custom error messages. They can be set in the
$validationMessages
property:
protected array $validationMessages;
And obtained by the getValidationMessages
method.
When create
, update
or replace
return false
, errors can be
retrieved via the getErrors
method.
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:
Create
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
.
$limit = 10;
$offset = 20;
$rows = $model->list($limit, $offset); // array
Update
The update
method updates based on the Primary Key and returns the number
of rows affected or false
if validation fails.
$id = 1;
$data = [
'name' => 'Johnny Doe',
];
$affectedRows = $model->update($id, $data); // int, string or false
Delete
The delete
method deletes based on the Primary Key and returns the number
of affected rows:
$id = 1;
$affectedRows = $model->delete($id); // int, string or false
Entities
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;
}
And, it can be instantiated as follows:
$user = new User([
'id' => 1,
'name' => 'John Doe',
]);
Populate
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.
protected URL $url;
protected string $name;
protected function init() : void
{
$this->name = $this->firstname . ' ' . $this->lastname;
$this->url = new URL('https://domain.tld/users/' . $this->id);
}
Magic Isset and Unset
To check if a property is set:
$isSet = isset($user->id); // bool
To remove a property:
unset($user->id);
Magic Getters
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
JSON Encoding
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
:
class User extends Entity
{
protected array $_jsonVars = [
'id',
'name',
'url',
'createdAt',
];
}
Once this is done, the entity can be encoded. Let's see in the following
example:
echo json_encode($user, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
And then the JSON object:
{
"id": 1,
"name": "John Doe",
"url": "https://domain.tld/users/1",
"createdAt": "2022-06-10T18:36:52-03:00"
}
Note that the url
and createdAt
property objects have been serialized.
Validator
The Framework\MVC\Validator class has additional rules that work, for example,
using database connections.
The following rules can be used alongside the
default validation rules:
exist
Requires that a value exists in the database.
exist:$tableColumn
exist:$tableColumn,$connection
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
.
existMany
Requires that many values exists in the database.
existMany:$tableColumn
existMany:$tableColumn,$connection
This rule is similar to exist. Except it is able to check if many values are
present in a database table.
It can validate many values from a select
HTML element:
<select name="fruits[]" multiple>
<option value="1">Apple</option>
<option value="2">Orange</option>
<option value="3">Pear</option>
<option value="5">Banana</option>
<option value="9">Strawberry</option>
</select>
The following example will validate if the ids sent in the fruits
field are
present in the Fruits
table:
existMany:Fruits.id
If any value does not exist, validation will fail.
unique
Requires that a value is not registered in the database.
unique:$tableColumn
unique:$tableColumn,$ignoreColumn,$ignoreValue
unique:$tableColumn,$ignoreColumn,$ignoreValue,$connection
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:
$file = 'home/index';
$data = [
'title' => 'Welcome!',
'description' => 'Welcome to Aplus MVC.',
];
echo $view->render($file, $data);
And the output will be like this:
<h1>Welcome!</h1>
<p>Welcome to Aplus MVC.</p>
Extending Layouts
The View has a basic layout system that other view files can extend.
Let's see the layout file views/_layouts/default.php below:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= $title ?></title>
</head>
<body>
<?= $view->renderBlock('contents') // string or null ?>
</body>
</html>
Then, in the view that will be rendered by the render
method, the file
_layouts/default
sets the content inside the contents
block:
views/home/index.php
<?php
$view->extends('_layouts/default'); // static
$view->block('contents'); // static
?>
<h1><?= $title ?></h1>
<p><?= $description ?></p>
<?php
$view->endBlock(); // static
If you want to extend views always from the same directory, you can set the
layout prefix:
$view->setLayoutPrefix('_layouts'); // static
This will make it unnecessary to type the entire path. See the example below:
- $view->extends('_layout/default');
+ $view->extends('default');
When working with only one file that extends a layout, it is possible to
set the default block name in the second argument of extends
.
Let's see how to extend the default layout and capture the content in the file
views/home/index.php:
<?php
$view->extends('default', 'contents'); // static
?>
<h1><?= $title ?></h1>
<p><?= $description ?></p>
So the rendered HTML file will look like this:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Welcome!</title>
</head>
<body>
<h1>Welcome!</h1>
<p>Welcome to Aplus MVC.</p>
</body>
</html>
View Includes
It is often common to have parts of the layout that are repeated. Like for example,
a header, a footer, a sidebar.
These files are called includes.
Let's see an example of include with a navigation bar in the file
views/_includes/navbar.php:
<div class="navbar">
<ul>
<li<?= $active === 'home' ? ' class="active"' : ''?>>
<a href="/">Home</a>
</li>
<li<?= $active === 'contact' ? ' class="active"' : ''?>>
<a href="/contact">Contact</a>
</li>
</ul>
</div>
This navbar will appear in several layouts, including the default one.
Let's see below how to make it appear in views that extend the layout
views/_layouts/default.php:
<body>
<?= $view->include('_includes/navbar') // string ?>
<h1><?= $title ?></h1>
As with layouts, you can set the includes path prefix:
$view->setIncludePrefix('_includes');
Once the includes path is set, it is no longer necessary to include it
in the include method call:
- $view->include('_includes/navbar');
+ $view->include('navbar');
The call in the default layout will be like this:
<body>
<?= $view->include('navbar') // string ?>
<h1><?= $title ?></h1>
When necessary, you can pass an array of data to the include.
Let's see how to pass the variable active
with the value home
:
<body>
<?= $view->include('navbar', ['active' => 'home']) // string ?>
<h1><?= $title ?></h1>
When rendered, the include will show the active
class on the Home line in the
navbar:
<div class="navbar">
<ul>
<li class="active">
<a href="/">Home</a>
</li>
<li>
<a href="/contact">Contact</a>
</li>
</ul>
</div>
View Blocks
Below we will see how to create a block called contents
and another
called scripts
in the views/home/index.php file:
<?php
$view->extends('default'); // static
$view->block('contents'); // static
?>
<h1><?= $title ?></h1>
<p><?= $description ?></p>
<?php
$view->endBlock(); // static
$view->block('scripts'); // static
?>
<script>
console.log('Hello!');
</script>
<?php
$view->endBlock(); // static
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' => 'required|minLength:5|maxLength:32',
'email' => 'required|email',
'message' => 'required|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 Instance
Each controller can have a $model
property, which will be automatically
instantiated in the class constructor.
The $model
property must have a child class type of Model.
Let's see below that $model
receives the type name of the
App\Models\UsersModel
class and in the show
method the direct call to
the $model
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 $model;
public function show(int $id) : string
{
$user = $this->model->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 $model;
public function index() : array
{
$page = $this->request->getGet('page')
$users = $this->model->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!';
}
}