Aplus Framework Docs

MVC

Aplus Framework MVC Library

Aplus Framework MVC (Model-View-Controller) Library.

Installation

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.

We can use the App::config method:

App::config()->setMany([
    'view' => [
        'default' => [
            'views_dir' => __DIR__ . '/views',
        ],
    ],
    'database' => [
        'default' => [
            'username' => 'root',
            'schema' => 'app',
        ],
        'replica' => [
            'host' => '192.168.1.10',
            'username' => 'root',
            'password' => 'Secr3tt',
            'schema' => 'app',
        ],
    ],
]);

Then the service config instances will be available.

Prints the contents of the view file __DIR__ . '/views/home/index.php':

echo App::view()->render('home/index');

Fetch all rows in the database default instance and move to the replica instance:

$result = App::database()->select()->from('Users')->run(); // Framework\Database\Result

while($user = $result->fetch()) {
    App::database('replica')->replace()->into('Users')->set($user)->run();
}

See config options at Services.

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',
            'token_bytes_length' => 8,
            'generate_token_function' => 'base64_encode',
            '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.

tokenbyteslength

Sets the length of random bytes used to generate the token. The default is 8.

generatetokenfunction

Sets the function to generate the token. Available values are: base64_encode, bin2hex, md5. The default is base64_encode.

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,
            'default_ttl' => null,
            '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.

defaultttl

Sets the default Time To Live, in seconds, for caching items.

The default value is null to use the class value (60).

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.

languageinstance

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,
            'search_engine' => 'google',
            '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.

languageinstance

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.

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',
        ],
    ],
]
autoloaderinstance

The Autoloader Service instance name. The default is 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' => [
            'host' => 'localhost',
            'port' => 587,
            'tls' => true,
            'username' => null,
            'password' => null,
            'charset' => 'utf-8',
            'crlf' => "\r\n",
            'keep_alive' => false,
        ],
    ],
]

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,
            'json_flags' => null,
        ],
    ],
]
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.

jsonflags

Flags for json_decode. The default is set to none.

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' => [],
            'json_flags' => null,
            'request_instance' => 'default',
        ],
    ],
]
headers

Sets an array where the keys are the name and the values are the header values. The default is to set none.

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.

languageinstance

Set Language Service instance name of the Language used in autolanguage.

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.

jsonflags

Flags for json_encode. The default is set to none.

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.

languageinstance

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',
        ],
    ],
]
savehandler

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.

languageinstance

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.

showdebugcomments

Set to false to disable HTML comments when in debug mode.

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->isMethod('get');
    }

    public function isPost() : bool
    {
        return $this->isMethod('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
Extra

The Model has some extra methods for doing common operations:

Count

A basic function to count rows in the table.

$count = $model->count(); // int

Optionally, with a parameter to the WHERE clause:

$where = [
    ['id', '<', 100], // WHERE `id` < 100
    ['name', 'like', 'Pa%'], // AND `name` LIKE 'Pa%'
];
$count = $model->count($where); // int
Replace

Replace based on Primary Key and return the number of affected rows or false if validation fails.

$id = 1;
$data = [
    'name' => 'John Morgan',
    'email' => '[email protected]',
];
$affectedRows = $model->replace($id, $data); // int, string or false
Save

Save a row. Updates if the Primary Key field is present, otherwise inserts a new row.

Returns the number of rows affected in updates as an integer. The LAST_INSERT_ID(), in inserts. Or false if validation fails.

$data = [
    'id' => 1,
    'email' => '[email protected]',
];
$result = $model->save($data); // 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
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',
]);

Result using default timezone (-03:00):

echo $user->createdAt->format('Y-m-d H:i:s'); // '2024-08-02 10:30:00'

When passing through the toModel method, the value of createdAt is modified.

The conversion to the time zone of the $_timezone property (+00:00) is performed:

$data = $user->toModel();
echo $data['createdAt']; // '2024-08-02 13:30:00'

When modifying $_timezone, the converted value will also change:

$user->_timezone = '+05:00';
$data = $user->toModel();
echo $data['createdAt']; // '2024-08-02 18: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;
}
$user = new User([
    'id' => 1,
    'name' => 'John Doe',
    'url' => 'https://domain.tld/users/1',
    'createdAt' => 'now',
]);

Or set whenever you want:

$user->_jsonVars = [
    'id',
    'name',
    'url',
    'createdAt',
    'updatedAt',
    'bio',
];

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": "2024-08-02T10:30:00-03:00",
    "bio": null
}

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;
}

You can find more details on the JSON Constants page.

Below is an example showing what can happen:

class People extends Entity
{
    protected string $name;
    protected float $height;
    protected string $status;
    protected URL $link;
    protected array $config;
}

$people = new People([
    'name' => 'John Doe',
    'height' => 1,
    'status' => 'Happy! ❤️ ⚡⚡',
    'link' => 'https://domain.tld/john-doe',
    'config' => [
        'theme' => [
            'color' => 'magenta',
            'background' => 'black',
        ],
    ],
]);

echo (string) $people;

This is the result when the People entity is encoded without flags:

{"name":"John Doe","height":1,"status":"Happy! \u2764\ufe0f \u26a1\u26a1","link":"https:\/\/domain.tld\/john-doe","config":{"theme":{"color":"magenta","background":"black"}}}

And, this is the result with the default flags:

{"name":"John Doe","height":1.0,"status":"Happy! ❤️ ⚡⚡","link":"https://domain.tld/john-doe","config":{"theme":{"color":"magenta","background":"black"}}}

Note that height has a float value, the unicode in status is not escaped and link has no backslashes.

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' => '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!

Search results